4.3 KiB
Fee Plans Backend
Purpose
fee_plans is the per-organization catalogue of fee plans (billing schedules) that invoices can
reference. It is a generic-CRUD slice assembled from the shared factories; the backend is the
source of truth for fee-plan records.
Slice Files (by layer)
- Route:
src/routes/fee_plans.ts—createCrudRouter(controller, { permission: 'fee_plans' }). - Controller:
src/api/controllers/fee_plans.controller.ts—createCrudController(service, { csvFields }). - Service (BLL):
src/services/fee_plans.ts—createCrudService(DbApi, { notFoundCode: 'fee_plansNotFound' }). - Repository (DAL):
src/db/api/fee_plans.ts(Fee_plansDBApi) — entity-specificcreate/bulkImport/update/findBy/findAll;remove/deleteByIds/findAllAutocompletedelegate todb/api/shared/repository.ts. - Model:
src/db/models/fee_plans.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).
API
The standard generic-CRUD surface (all under /api/fee_plans, JWT + ${METHOD}_FEE_PLANS
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 }]wherelabelisname.GET /:id— returns the record with eager associations (see Data Contract).
csvFields: id, name, notes, total_amount.
Access Rules
- JWT required; the whole router is guarded by
checkCrudPermissions('fee_plans'), derivingREAD_FEE_PLANS/CREATE_FEE_PLANS/UPDATE_FEE_PLANS/DELETE_FEE_PLANSper 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),name,notes(TEXT, nullable).billing_cycle— ENUMone_time|monthly|termly|annual.total_amount— DECIMAL.active— BOOLEAN,allowNull: false, defaultfalse.importHash(unique),academic_yearId,organizationId,gradeId,createdById,updatedById, timestamps.
Associations: belongsTo organization, academic_year, grade, createdBy/updatedBy (users);
hasMany invoices_fee_plan (invoices). findBy/GET /:id eager-load invoices_fee_plan,
organization, academic_year, grade in a single Promise.all.
List filters (FeePlansFilter): id, name, notes, total_amountRange, active,
billing_cycle, academic_year (id or name, |-separated), grade (id or name, |-separated),
organization, createdAtRange, plus field/sort ordering and limit/page pagination.
Behavior / Notes
- This slice has a real
activeBOOLEAN column.create/bulkImportdefaultactivetofalse;updatesets it when provided. bulkImportoffsetscreatedAtper row byBULK_IMPORT_TIMESTAMP_STEP_MSto preserve order.- List pagination uses the shared
resolvePaginationdefaults (page size 10, capped at 100). - Note:
findAllapplies theactivefilter twice — once via the sharedfilter.active === true || filter.active === 'true'coercion and again via a redundantif (filter.active) where.active = filter.activeblock (kept for source accuracy).
Tests
None yet.
Related
- Generic-CRUD contract:
backend-architecture.md; related slices:invoices,academic_years,grades,permissions.md.