Skoolsewa - School Management System Docs
Developer ResourcesAccounting integration

Accounting Integration Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the accounting integration module.

Accounting Integration Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Schemapackages/db/src/schema/integration.tsEvery table, enum, and constraint
Authapps/api/src/modules/integration/auth/*.tsAPI key issuance, verification, rate limiting
Webhooksapps/api/src/modules/integration/webhooks/*.tsEndpoint lifecycle, dispatch, delivery, retry
Feedapps/api/src/modules/integration/feed/integration-feed.service.tsCursor semantics, epoch
Restoreapps/api/src/modules/integration/restore/integration-restore-reconciler.service.tsWhat a backup restore does to credentials
External APIapps/api/src/modules/integration/fees-api/**The accounting application's own read/write surface
Admin APIapps/api/src/modules/integration/clients/**The office's client/key/webhook administration screens
Envapps/api/src/config/env.validation.tsINTEGRATION_SECRET_KEYS provisioning

2. Feature Summary

FieldValue
Moduleintegration
Submodulesclients (admin), auth, feed, keyring, webhooks, restore, fees-api (the accounting application's own surface)
Primary user valueA school's accounting application reads fee data and reports payments without staff re-entering anything, and the school office can see and control exactly what that application can do.
ActorsOffice/superadmin (client administration), the accounting application (API key holder), the daily sweep/webhook worker
Main entry points/api/integration-clients/** (admin), /api/integration/fees/** (the accounting application), QueueName.INTEGRATION_WEBHOOKS
Main outputsIssued API keys and webhook secrets (shown once), delivered webhooks, the change feed, payment sync results, mappings
Related docsBackend, API. Fee data itself is documented in the Fees module.

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
Office (holding FeeIntegrations_*, superadmin-granted)Register/suspend/revoke a client, issue/revoke its keys, manage its webhooks, read feed statusSee a key or webhook secret after its one issuing responseJWT + RoleGuard + FeeIntegrations_READ/_CREATE/_UPDATEFeeIntegrations is SUPERADMIN_ONLY — never in the automatic admin grant
Accounting applicationRead the fee catalogue/structures/scholarships/charges/students/obligations, report payments, manage its own student/invoice mappings, poll the change feed, receive signed webhooksWrite anything but payments and mappings; act as any client but the one its key belongs to; see events private to another clientAuthorization: Bearer ssk_… + scopeNever a staff JWT — its own authentication layer entirely
Webhook receiver (the accounting app's own HTTP endpoint)Receive signed POSTsTrust an unsigned or stale requestVerifies X-Skoolsewa-Signature itselfThe API is the sender, not this actor's authenticator
Sweep / workerDeliver webhooks, retry failed deliveries, recover overdue ones, fan events outAnything requiring office judgementInternalIntegrationWebhookDeliveryService, FeeQueueProcessor's webhooks.recover() call
System (backup restore)Force a new feed epoch, revoke every active key, pause every active webhook—Internal, inside the restore transactionIntegrationRestoreReconciler

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState Written
Register a clientAdminOfficePOST /api/integration-clientsintegration_clients
Suspend/activate/revokeAdminOfficePOST .../{suspend,activate,revoke}integration_clients; revoke cascades keys+webhooks
Issue/revoke a keyAdminOfficePOST .../keys, .../keys/:keyId/revokeintegration_client_keys
Create/update/rotate/test a webhookAdminOfficePOST/PATCH .../webhooks/**integration_webhook_endpoints
Retry a deliveryAdminOfficePOST .../webhook-deliveries/:id/retryintegration_webhook_deliveries
Read the catalogue/structures/modifiersAccounting appSystemGET /api/integration/fees/{categories,components,structures,scholarships,charges}— (read-only)
Read the school's own identity and contact factsAccounting appSystemGET /api/integration/fees/school— (read-only; bankName/bankAccountNumber need payroll:bank:read in addition to fees:read)
Read students/obligationsAccounting appSystemGET /api/integration/fees/{students,obligations}—
Report a paymentAccounting appSystemPOST /api/integration/fees/paymentsfee_payment_sync_events (see Fees module)
Manage mappingsAccounting appSystemPUT /api/integration/fees/{student,invoice}-mappings/:idfee_external_student_mappings/fee_external_invoice_mappings
Poll the change feedAccounting appSystemGET /api/integration/fees/changesRedis poll marker
Receive a webhookAccounting app's receiverSystemSigned POST—
Deliver/retry a webhookWorkerSystemIntegrationWebhookJob.DELIVERintegration_webhook_deliveries
Reconcile after a restoreSystemBackup restoreIntegrationRestoreReconciler.reconcileNew feed epoch, keys revoked, webhooks paused

5. User-Facing Flows

5.1 Registering a new accounting client and issuing its first key

Summary

The office creates one client per external accounting system, scopes it, then issues an API key the accounting vendor configures into their application.

Preconditions

  • ACCOUNTING_INTEGRATION_ENABLED is true on the server (otherwise creating a client or issuing a key answers 404 ACCOUNTING_INTEGRATION_DISABLED).
  • INTEGRATION_SECRET_KEYS is set on the server (otherwise every write here answers 409 INTEGRATION_NOT_CONFIGURED).
  • The caller holds FeeIntegrations_CREATE.

Main Flow

StepActor/SystemActionResultSource
1OfficePOST /api/integration-clients with name, scopes, optional rate limitA client row, status: activeIntegrationClientsService.create
2OfficePOST /api/integration-clients/:id/keysA key issued; the raw token returned ONCE in the response body, Cache-Control: no-storeIntegrationKeysService.issue
3OfficeCopies the token into the accounting vendor's configuration——
4Accounting appGET /api/integration/fees/ping with the tokenConfirms the key, scopes, server time, and feed epochintegration-fees-meta.controller.ts

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Integration not configuredINTEGRATION_SECRET_KEYS unsetEvery credential-issuing write refused409 INTEGRATION_NOT_CONFIGURED; reads that need no new credential still work
Duplicate nameAnother client already has this name (case-insensitive)Refused before any write409 INTEGRATION_CLIENT_NAME_TAKEN
Unknown scopeA scope not in INTEGRATION_SCOPESRefused400 VALIDATION_FAILED
Third active keyA client already holds 2 active, unexpired keysRefused — rotate or revoke one first409 INTEGRATION_KEY_LIMIT
Key rotationreplacesKeyId passed on issueThe old key's life is shortened to a 7-day grace window rather than killed immediatelyBoth keys authenticate during the grace period

5.2 The accounting application reports a payment

Summary

The vendor's system posts a payment event whenever money moves on their side; the API acknowledges receipt immediately and processes it asynchronously.

Main Flow

StepActor/SystemActionResultSource
1Accounting appPOST /api/integration/fees/payments with eventId, transactionRef, status, amountPaisa, externalUpdatedAt, and an identity hint202 with {syncEventId, replayed}IntegrationFeesPaymentsController
2BackendMatches identity (obligation/student/invoice ref) against mappings; writes/updates fee_external_transactionsApplied, held, or rejected — see the Fees module's payment sync docsFeeSyncProcessorService (owned by fees, invoked here)
3Accounting appGET /api/integration/fees/payments/events/:eventIdThe event's current processing stateIntegrationFeesPaymentsController

Branches and Edge Cases

BranchConditionBehaviorError/Result
ReplayThe same eventId posted againNo second write; replayed: true202, same syncEventId
Reused eventId, different bodySame eventId, different payloadRefused — not a true replayFEE_SYNC_EVENT_ID_REUSED (see Fees module)
Body too largeContent-Length over 32 KBRefused before the body is read413 INTEGRATION_PAYLOAD_TOO_LARGE
Wrong scopeThe key lacks payments:writeRefused403 INTEGRATION_SCOPE_MISSING
Rate limitedOver the client's per-minute budgetRefused with Retry-After429, header set

5.3 Receiving a webhook

Summary

Whenever a subscribed change happens, the school's server signs and POSTs a thin event to every live endpoint the client has registered for it.

Main Flow

StepActor/SystemActionResultSource
1SystemA fee/structure/modifier/payment/mapping change writes an integration_change_events row in the SAME transaction as the business changeThe change is durable before anything is sentFeeChangeFeedService.record (Fees module)
2WorkerFeeJob.CHANGE_FANOUT reads subscribed, live endpoints for the event's scopeOne integration_webhook_deliveries row per matching endpointIntegrationWebhookFanoutService
3WorkerIntegrationWebhookJob.DELIVER POSTs {id, type, occurredAt, resource, academicSessionId}, signed2xx → succeeded; else scheduled for retry per the backoff tableIntegrationWebhookDeliveryService
4Accounting app receiverVerifies X-Skoolsewa-Signature, processes, fetches the full resource by its public id if neededThe event never carries the resource itself—

Branches and Edge Cases

BranchConditionBehaviorError/Result
Endpoint pausedThe endpoint's status is pausedThe delivery is HELD an hour, no attempt spent—
Client suspendedThe owning client is not activeSame — held, not killed—
Endpoint disabledstatus = disabledThe delivery dies immediatelylastError: "endpoint_disabled"
Nine failed attemptsEvery backoff step exhausteddead, a job_failures row, office notifiedintegration.webhook_dead.admin
50 dead deliveries in a rowconsecutiveFailures reaches the thresholdThe endpoint auto-disables itselfintegration.webhook_disabled.admin
Secret rotationA rotation is in its 24-hour grace windowDeliveries are signed with BOTH secretsThe header carries two v1= values
Client-private evente.g. student_mapping.createdDelivered only to the client the mapping belongs toNever fanned to every subscriber

6. Admin Flows

6.1 Revoking a client (terminal)

6.2 Rotating a webhook secret

Route: POST /api/integration-clients/:id/webhooks/:webhookId/rotate-secret. The previous secret keeps signing for 24 hours (previous_secret_ciphertext/previous_valid_until), so a receiver switching secrets never drops an event mid-rotation. The new secret is shown once, Cache-Control: no-store.

7. Lifecycle and State Transitions

7.1 Integration client

FromEventToGuardSide Effects
activeSuspendsuspended—Keys stop authenticating; webhooks hold their deliveries
suspendedActivateactive——
active/suspendedRevokerevokedReason requiredTerminal; cascades key revocation and webhook disabling

7.2 API key

active (issued) → revoked (manual revoke, client revocation, or a restore). A key's expires_at (365 days) is a second, independent path to unusability that never changes status — usable() checks both.

7.3 Webhook endpoint

FromEventToSide Effects
activePausepausedDeliveries hold, no attempt spent
pausedRe-enableactivedispatch_generation bumped, consecutive_failures reset
active/pausedDisable (manual or auto at 50 dead)disabledDeliveries die on next attempt
disabledRe-enableactiveSame as above

7.4 Webhook delivery

9. Data and Side Effects by Flow

FlowDB WritesJobsNotifications
Issue a keyintegration_client_keys——
Create a webhookintegration_webhook_endpoints——
A fee changeintegration_change_events (Fees module)CHANGE_FANOUT—
Fan-outintegration_webhook_deliveriesIntegrationWebhookJob.DELIVER per delivery—
Delivery deadintegration_webhook_deliveries, job_failures—integration.webhook_dead.admin
Endpoint auto-disabledintegration_webhook_endpoints—integration.webhook_disabled.admin
RestoreNew feed epoch, keys revoked, webhooks paused—integration.restored.admin

10. Error and Recovery Flows

ScenarioTriggerRecovery
Integration not configuredINTEGRATION_SECRET_KEYS unsetOperator sets the env var, restarts; existing (already-issued) credentials remain unusable until then
Key limit reached2 active keys alreadyOffice revokes or rotates one before issuing a third
Webhook delivery exhausted9 attempts, all failedOffice fixes the receiver, then POST .../retry, or waits for the daily recovery pass
RestoreA backup is restoredEvery credential invalidated by construction; office re-issues what the accounting app needs. If reconciling the integration fails, the site stays in maintenance rather than lifting it over stale credentials — restarting the API retries reconciliation at boot
Restore outside the normal flowSomeone restores the database by hand (pg_restore onto the same cluster), bypassing the backup-restore workflowDetected at the next boot from either an active key/client the system already recorded revoking, or the change feed having served further than the restored data now goes — reconciled the same way as a workflow-driven restore
Accounting integration switched offAn operator sets ACCOUNTING_INTEGRATION_ENABLED=false and restartsThe integration API, the fees/salary/asset accounting-facing admin routes, and every client/key/webhook-creating admin action answer 404 ACCOUNTING_INTEGRATION_DISABLED; listing, suspending, revoking a client, and revoking a key stay reachable. Nothing new is written to the change feed or delivered as a webhook, and every outstanding delivery is killed rather than retried. Turning the switch back on starts a fresh feed epoch, so the accounting app resyncs from the current state instead of trying to resume a stale cursor

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorTriggerResultSource
Key authenticationConstant-time comparison regardless of whether the prefix existsEvery requestAn unknown prefix and a wrong secret answer identically (401)IntegrationKeysService.authenticate
Key last_used_atWritten at most once a minuteEvery authenticated requestAvoids a write storm on a busy integrationIntegrationKeysService.touch
Rate limitPer-client, per-minute, Retry-After headerEvery request over budget429IntegrationRateLimiterService
IP throttle600/min per IP, AHEAD of authenticationEvery requestBounds unauthenticated noise before a key is even checkedIntegrationEndpoint decorator
Scope-less routeA route with no declared scopeAny such route (should not exist)Refused outright, logged as an errorIntegrationApiKeyGuard
Body size cap>32 KB declared Content-LengthAny POST/PUT413, unreadIntegrationApiKeyGuard
Cursor canonicalizationA non-canonical base64url cursorAny keyset-paged GET400 INTEGRATION_CURSOR_INVALID — never a best-effort guessintegration-fees-keyset.util.ts
Change feed epochA cursor from before a restoreGET .../changes410 INTEGRATION_CURSOR_EXPIRED — resync from scratchIntegrationFeedService
Private event visibilitystudent_mapping.* eventsThe feed and fan-outVisible/delivered only to the owning clientINTEGRATION_CLIENT_PRIVATE_EVENT_TYPES
Two-secret signingA rotation's 24-hour grace windowEvery delivery in that windowThe header carries two v1= signatureswebhookSignatureHeader

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonEnforced InTests
A client is never re-created for the same accounting systemPayment sync idempotency (eventId) is per clientintegration_clients is keyed by name uniqueness, keys/webhooks rotate INSIDE it—
A key's secret is never recoverableOnly an HMAC under a server pepper is storedintegration_client_keys.secret_hashapi-key.util.spec.ts
A webhook secret must be decryptable (not just verifiable)The server signs outbound requests with itAES-256-GCM under the keyring, not a one-way hashsecret-box.util.spec.ts
A restore invalidates every credentialData issued before a restore predates the restored state by constructionIntegrationRestoreReconciler—
Scholarship/charge lines never leak an internal idPublic surfaces reference public_id onlyEvery fees-api mapper—

12.5 Flow Edge-Case Matrix

FlowEdge CaseExpected BehaviorSource
AuthenticateMalformed Authorization header401 INTEGRATION_AUTH_INVALID, same as a wrong secretIntegrationApiKeyGuard
Webhook create6th endpoint for one client409 INTEGRATION_WEBHOOK_LIMITIntegrationWebhookEndpointsService
Webhook createURL with credentials or a fragment400 INTEGRATION_WEBHOOK_URL_INVALIDwebhook-target.util.ts
Webhook createhttp:// in productionRefused unless INTEGRATION_WEBHOOK_ALLOW_PRIVATE_TARGETS=trueSame
Mapping upsertA student already mappedOld mapping retired, new one created — never an in-place overwritefee_external_student_mappings (Fees module)
Feed readNo cursor suppliedStarts from the beginning of what is visible below the snapshot's xminIntegrationFeedService.read

13. Completion Checklist

  • Every client/key/webhook/feed/restore capability is listed.
  • Office and accounting-application actors both have allowed/forbidden behavior.
  • Major flows (register, issue key, report payment, receive webhook, restore) have steps, branches, and diagrams.
  • Client, key, webhook, and delivery lifecycles each have a transition table and state diagram.
  • Flows link to the API and backend docs.

See Also

  • Backend doc: /docs/developer/integration/backend
  • API doc: /docs/developer/integration/api
  • Fee data itself: /docs/developer/fees/feature