4.6 KiB
Invoices Backend
Purpose
invoices is the per-organization billing-invoice ledger. It is a generic-CRUD slice assembled
from the shared factories; the backend is the source of truth for invoice records.
Slice Files (by layer)
- Route:
src/routes/invoices.ts—createCrudRouter(controller, { permission: 'invoices' }). - Controller:
src/api/controllers/invoices.controller.ts—createCrudController(service, { csvFields }). - Service (BLL):
src/services/invoices.ts—createCrudService(DbApi, { notFoundCode: 'invoicesNotFound' }). - Repository (DAL):
src/db/api/invoices.ts(InvoicesDBApi) — entity-specificcreate/bulkImport/update/findBy/findAll;remove/deleteByIds/findAllAutocompletedelegate todb/api/shared/repository.ts. - Model:
src/db/models/invoices.ts. - Shared used: CRUD factories (
services/shared/crud-service.ts,api/controllers/shared/crud-controller.ts,api/http/crud-router.ts), repository helpers (db/api/shared/repository.ts),shared/constants/pagination.ts(resolvePagination),db/api/file.ts(replaceRelationFilesfor theattachmentsrelation).
API
The standard generic-CRUD surface (all under /api/invoices, JWT + ${METHOD}_INVOICES
permission, all 200) — see backend-architecture.md for the shared contract:
POST /— body{ data }, returnstrue.POST /bulk-import— multipart CSV file, returnstrue.PUT /:id— body{ data, id }(the service reads the id from the body, not the path param), returnstrue.DELETE /:id— returnstrue.POST /deleteByIds— body{ data: string[] }, returnstrue.GET /— query filters, returns{ rows, count };?filetype=csvstreams a CSV ofcsvFields.GET /count— returns{ rows: [], count }.GET /autocomplete—?query&limit&offset, returns[{ id, label }]wherelabelisinvoice_number.GET /:id— returns the record with eager associations (see Data Contract).
csvFields: id, invoice_number, notes, subtotal, discount_amount, tax_amount,
total_amount, balance_due, issue_date, due_date.
Access Rules
- JWT required; the whole router is guarded by
checkCrudPermissions('invoices'), derivingREAD_INVOICES/CREATE_INVOICES/UPDATE_INVOICES/DELETE_INVOICESper HTTP method. - Access is granted by role permission or per-user
custom_permissions(seepermissions.md).
Tenant Scope
findAllscopeswhere.organizationIdtocurrentUser.organizationId; aglobalAccessrole clears the org filter (sees all tenants).createassigns the organization fromcurrentUser.organizationId;updateonly reassigns organization forglobalAccessusers (otherwise it stays the caller's org).
Data Contract
Model columns (paranoid, soft-delete via deletedAt):
id(UUID PK),invoice_number,notes(TEXT, nullable).issue_date,due_date— DATE.subtotal,discount_amount,tax_amount,total_amount,balance_due— DECIMAL.status— ENUMdraft|issued|partially_paid|paid|overdue|void.importHash(unique),campusId,fee_planId,organizationId,studentId,createdById,updatedById, timestamps.
Associations: belongsTo organization, campus, student, fee_plan, createdBy/updatedBy (users);
hasMany payments_invoice (payments); hasMany file as attachments (scoped relation).
findBy/GET /:id eager-load payments_invoice, organization, campus, student, fee_plan,
attachments in a single Promise.all.
List filters (InvoicesFilter): id, invoice_number, notes, issue_dateRange,
due_dateRange, subtotalRange, discount_amountRange, tax_amountRange, total_amountRange,
balance_dueRange, status, campus (id or name, |-separated), student (id or
student_number, |-separated), fee_plan (id or name, |-separated), organization,
createdAtRange, plus field/sort ordering and limit/page pagination.
Behavior / Notes
create/bulkImport/updatemanage theattachmentsfile relation viaFileDBApi.replaceRelationFiles.bulkImportoffsetscreatedAtper row byBULK_IMPORT_TIMESTAMP_STEP_MSto preserve order.- List pagination uses the shared
resolvePaginationdefaults (page size 10, capped at 100). - Note:
InvoicesFilteraccepts anactiveflag the model has no column for; it is currently inert (kept for source accuracy).
Tests
None yet.
Related
- Generic-CRUD contract:
backend-architecture.md; related slices:payments,fee_plans,students,campuses,file.md,permissions.md.