Skip to main content

EMP – InvoicePro

EMP – InvoicePro (product title Finance Pro, repo folder finance-pro, package names @finance-pro/server / @finance-pro/client) is an AP automation and finance reconciliation platform: vendor invoice processing, InterCompany reconciliation, and bank reconciliation.

This page is a source-level analysis of the repository: routes, controllers, workers, services, schema, n8n workflow JSON and Airflow DAGs. Everything here was read from code. Items still unverified are listed in §7.

Provenance
  • GitLab repository: emp/invoicepro (SSH: ssh://git@gitlab.tsgitdev.com:2222/emp/invoicepro.git).
  • Analysed revision: branch develop @ 926cae40 (add oracle test, 2026-10-02), version 1.0.1.
  • Original snapshot doc: finance-pro-business-and-code-flow.md (dated 2026-09-16).
  • Claims were re-verified against the working tree at the revision above. Where the code has since drifted, the difference is called out inline.

Contents​

  1. System overview
  2. Business flow
  3. Code flow
  4. Deep dives
  5. Findings: risks and tech debt
  6. Mapping to V2 workstreams
  7. Not yet verified
  8. Repository reference

1. System overview​

Two databases. The main DB holds transactional app data. The data layer holds Oracle-synced reference data. Graphile workers copy reference data from the data layer into the main DB (fas_*, commitments, activations, …), and the app reads those copies.

Three business modules: AP Invoice Processing (the core), InterCompany reconciliation, and Bank reconciliation.


2. Business flow​

2.1 Actors and roles​

Role (code)Business meaningKey powers
adminSuper adminEverything
ap-adminAP administratorDoc sets, users, approval config
ap-userAP clerkProcess doc sets, request approval
ap-approverAP approverApproves via email token; can also create approval requests
fin-admin / fin-user / fin-approverFinance (InterCompany)fin-user edits contra entries; fin-approver / fin-admin approve
viewerRead-onlyBlocked from mutations by inline checks
Bank-recon usersGrant table bank_reconciliation_permissionsAccess to bank recon (admins always have it)
SYSTEM / AUTOMATION_USERAutomation identityOwns auto-created doc sets and auto-ignored contras

Positions (ap_staff, ap_section_head, ap_coordinator, ap_department_head) exist in constants/roles.ts. ap-approver is a valid role but has no entry in casbin-policy.csv.

2.2 Reference data the business depends on​

ConceptWhat it isSource
CommitmentPO / SO / Contract (approved value, vendor, account codes)production.commitments view (MMIS / PRIS / …)
ActivationProof of delivery: BAST (PRIS), MRR (SAP B1), etc. Keyed by DO/GR no. + PO no.production.activations view
FAS ContractFAS contract master: supplier code, company, credit days, validityin_fas_mst_contract
Account rulesCOA combinations (main / sub1 / sub2), descriptions, IC flagin_fas_mst_v_account_rule
Currency conversionnuRbconvFactor per currency, company and datein_fas_currency_conv
FAS transactionsPosted journals (InterCompany, Bank Recon)in_fas_dt_transaction
Bank masterBank account to COA mappingin_fas_mst_bank

2.3 Module A: AP Invoice Processing (end-to-end)​

Goal: a vendor invoice is paid only when invoice, PO/contract and goods receipt agree. The resulting journal is posted to Oracle FAS with an approval trail.

Business rules as implemented:

  1. Intake. Emails are threaded into conversations via the References and In-Reply-To headers. The vendor is identified by sender address against vendor_emails. Unknown senders are still ingested, but a doc set is only auto-created once some email in the thread has a vendor_id. Inbox users can verify a sender or link it to a vendor. Reply emails in an already-handled thread update the existing invoice data instead of creating a new set.

  2. Document types. Exactly three types drive the process: invoice, tax receipt (Faktur Pajak) and goods receipt. Everything else is other (preview only).

  3. Auto-creation criteria (create-document-set-cron):

    • an invoice, plus at least one goods receipt;
    • the tax receipt is optional;
    • nothing still extracting;
    • the vendor is known;
    • the invoice number does not already exist (otherwise the conversation goes to CONFIRM).

    The company is guessed by fuzzy-matching the invoice buyer name. The number comes from generateDocumentSetNumber() (e.g. INFP2500001).

  4. Validation. Users correct OCR fields and line items, re-extract or replace files, and mark documents validated. Any data edit pushes the set back to validating.

  5. 3-way matching. It cannot start until:

    • Contract/PO/SO, FAS contract and DO numbers are set;
    • the commitment exists;
    • the FAS contract exists for the company;
    • every DO/PO pair exists in activations.

    The rules run in the n8n workflow 3-Way matching new:

    RuleCheckNotes
    3WM-00001PO/SO/Contract no. consistent across contract, GR and invoiceFuzzy, 85% similarity to tolerate OCR typos
    3WM-00002GR numbers on invoice match the selected/uploaded GRs
    3WM-00003Vendor name fuzzy match across contract, GR, invoice, tax receiptTax receipt skipped if overridden
    3WM-00004–07Line items: invoice vs GR vs commitment (description, qty, unit price, amount)LLM-based (Bedrock); count mismatch = error
    3WM-00008Invoice value = Σ GR payment values, and ≤ remaining contract amount
    3WM-00009GR delivery date within contract validityWarning
    Rule ID drift

    The n8n workflow JSON at this revision defines 3WM-00001, -00002, -00003, -00004, -00007, -00008, -00009. Rules -00005 and -00006 are not declared as separate IDs; the line-item family runs as one block.

    Each issue carries severity (error / warning / info), isError, and an override that is nullable. Gate to Mark Ready (client-side): every issue with isError = true and severity error or warning must have an override with a reason of at least 10 characters. The tax receipt can be overridden separately.

  6. Journal voucher (generated on Mark Ready, editable afterwards):

    • Expense (Dr): Σ invoice line items. Account comes from the commitment's account / sub-account / level-3 code.
    • Tax by the first three digits of the tax slip code:
      • 040 Normal: tax expense Dr only.
      • 030 WAPU: tax expense Dr plus tax payable 213105 Cr.
      • 070 Exempt: no tax lines.
    • Tax expense account by company:
      • Tonga A100: 113303-00000-TBPMIGA.
      • Korinci A030: 113303-000000-TBPMIGA.
      • Corporate A9xx (not A901) with prefix 040: 113321-GRUP/GUSD-X110.
      • Otherwise: the PRIS AFE account.
    • AP (Cr, balancing): 211111 (IDR) or 211112 (USD), sub1 00000, sub2 = supplier code from the FAS contract.
    • Medical invoices use a separate generator (§4.3).
  7. Approval.

    • Approvers: passed in explicitly or auto-resolved. Auto-resolution converts the invoice subtotal (before tax) to USD (amount / nuRbconvFactor), then picks the highest tier assigned to the company with minAmount ≤ amountUSD. That tier's ordered approvers are used.
    • Sequencing: strictly sequential. Each approver gets a random 64-byte token, valid 168h by default. Any single rejection rejects the request.
    • Admin actions: resend, restart, reassign, send back to processing.
  8. Posting. Background job with retries. Visible in invoice_fas_export_logs and voucher_headers (postedToOracle, oracleCopyStatus, vcVoucherNo).

Bill date semantics

document_sets.bill_date is set to now when matching starts, not the invoice date. It drives the FX rate lookup, FAS dt_bill_date, and dt_due_date = bill_date + contract credit days.

Status models:

EntityStatuses
Conversationnew → filecat → fileext → done, plus confirm (duplicate) and error (failed attachment or missing invoice/GR). Full history in status_history.
Attachmentpending → extracting → completed / failed
Documentextraction_status: pending / processing / completed / failed
Document setvalidating → matching → ready → in_approval → approved
Approval + itemsreview_approvers → pending → approved / rejected
FAS export logpending → exporting → success / failed

2.4 Module B: InterCompany reconciliation​

Goal: every intercompany journal posted in FAS gets an offsetting (contra) entry in the counterpart entity, unless it is already reconciled.

Contra statuses: pending → pending_for_approval → approved, plus rejected and ignored.

2.5 Module C: Bank reconciliation​

Goal: match each bank statement line to an FAS bank journal for one entity, account and month, and isolate bank charges.

Match methods: auto_value, auto_voucher, auto_llm, auto_bank_charge, manual. Result: matched or amount_mismatch.


3. Code flow​

3.1 Repository layout​

server/src/
index.ts API bootstrap (Express, workerUtils for enqueueing)
worker.ts Graphile Worker runner: taskList + crontab
routes.ts All routes (~200), wired to controllers
schema.ts Drizzle schema + status constants + Zod (~2.9k lines)
storage.ts Repository layer, one large class (~238 KB)
auth.ts JWT sign/verify (6h expiry), bcrypt
controllers/ HTTP handlers (much business logic lives here)
services/ voucher-generator, medical-voucher-generator, fas-export,
intercompany-export, approval-config, bank-recon-*, storage (S3/local)
workers/ Graphile tasks
scripts/ sync-fas-data, sync-commitments-data, bank extractors, migrations
matching-service.ts Calls n8n 3-way-match
casbin-* RBAC model/policy
client/src/
app/ layout, providers (auth, react-query, i18n)
modules/ document-sets, inbox, admin, bank-reconciliation, dashboard, settings
pages/approvals/ internal list + public token approval page
components/intercompany/
shared/lib/api-client.ts fetch wrapper, JWT from localStorage
n8n/workflows/ Email ingest, Attachment Categorization, Document Extractor, 3-Way matching
airflow/dags/ oracle_daily, oracle_transaction, sapb1, sharepoint, finpro_daily, writeback (legacy), backup
nginx/conf.d/ invoice-pro, workflow (n8n), airflow vhosts

3.2 Runtime processes​

ProcessEntryResponsibility
APIserver/src/index.tsREST, webhooks, SSE. Creates makeWorkerUtils to enqueue jobs. Also runs bank-recon matching in-process.
Workerserver/src/worker.tsBackground tasks and crons (WORKER_CONCURRENCY, default 5)
n8ndockerIMAP polling, AI classification and extraction, 3-way matching
Airflowseparate composeOracle / SAP / SharePoint → data layer
Nginxnginx/TLS, routing to SPA / API / n8n / Airflow

3.3 Request lifecycle (API)​

  1. express.json / urlencoded.
  2. Request logger (logs /api calls with truncated response JSON).
  3. Route-level middleware:
    • requireAuth: JWT from Authorization: Bearer or ?token= query param. The decoded claims become req.user with no DB lookup.
    • requirePermission(obj, act): Casbin, used only on some intercompany routes.
    • requireBankReconAccess: admin roles, or a row in bank_reconciliation_permissions.
    • requireAdmin.
    • requireWebhookAuth: header x-webhook-token equals WEBHOOK_AUTH_TOKEN.
    • multer upload: to disk under uploads/, or to memory for bank files.
  4. Controller. Most authorization is inline (e.g. role === viewer → 403, company-membership checks) rather than Casbin.

3.4 Flow 1: Email ingestion → classification → extraction​

Key files: controllers/webhooks.controller.ts, controllers/helpers/email-document-processing.helper.ts, storage.deriveConversationStatus().

3.5 Flow 2: Document set auto-creation​

workers/create-document-set-cron.ts runs as a Graphile cron every minute and self-re-enqueues every 20s (jobKey, mode "replace"):

  1. A raw SQL query selects up to 10 eligible conversations (criteria in §2.3).
  2. For each one:
    • duplicate-invoice check, otherwise insert document_sets;
    • insert documents for the latest invoice, latest tax receipt and all GRs;
    • saveExtractedData() fills invoice_data / tax_receipt_data / goods_receipt_data / invoice_line_items;
    • audit logs;
    • documentSetHandled = true, status DONE.

Two other creation paths exist:

  • workers/create-document-set.ts, an event job enqueued by Inbox → Reprocess;
  • inbox.controller.createDocumentSetFromConversation, a manual action with a per-conversation lock that also requires a tax receipt.

3.6 Flow 3: Manual upload and re-extraction​

  1. POST /api/documents/upload (multer).
  2. Existing invoice and tax receipt documents of the same type are soft-deleted.
  3. The file is stored.
  4. An extract-document job is queued (maxAttempts 3).
  5. The worker downloads from S3, converts PDFs to images (max 4 files × 4 pages, resized), and POSTs them to EXTRACTION_SERVICE_URL_<TYPE>.
  6. n8n calls back POST /api/webhooks/extraction-complete.
  7. The callback maps fields into the typed tables, replaces line items, writes a field-level audit diff, and broadcasts over SSE (/api/extraction-events).
  8. The UI also polls every 5s.

3.7 Flow 4: Validation → 3-way matching​

POST /:id/matching-results (webhook auth) is an alternative async callback. The synchronous path above does not use it.

3.8 Flow 5: Mark Ready and voucher​

POST /:id/mark-ready:

  1. Allowed only from matching. No server-side check of unresolved issues.
  2. fasContract = contractPoSoNumber.
  3. generateVoucherEntriesInternal(), best-effort:
    • invoice, tax receipt, line items;
    • company → FAS currency code → conversion factor at bill date (invoice currency and IDR);
    • FAS contract (vcCompCode, vcSuppCode), activations;
    • generateMedicalVoucherEntries() or generateVoucherEntries().
  4. Status ready, and the result goes to document_sets.voucher_entries (JSONB).

Users then edit via PUT /:id/voucher-entries (blocked once approved) or regenerate via POST /:id/voucher-entries/generate.

3.9 Flow 6: Approval​

StepEndpointEffect
ResolveGET /api/approval-config/resolve/:documentSetIdTier and approvers by USD amount
CreatePOST /api/approvalsApproval + items = review_approvers; doc set = in_approval
SendPOST /api/approvals/:id/send-for-approvalApproval = pending; first item = pending; token row; email_send_logs row; send-email job (Nodemailer, 3 attempts)
Approver viewGET /api/approval-tokens/:token (public)SPA route /approval-preview/:token
ActPOST /api/approval-tokens/:token/processItem approved/rejected; token usedAt; calculateApprovalStatus(). Approved with items left: next item pending + email. All approved: doc set approved + enqueueFasExport().
Adminresend / restart / reassign / send-back-to-processing / retry-fas-exportSend-back deletes approval, items and tokens; doc set → ready

The approval list reads a materialized view refreshed by refresh-approval-list-view (about every 10s).

3.10 Flow 7: FAS export​

workers/export-to-fas.ts → services/fas-export.service.ts:

  1. Export log → exporting.
  2. mapToFasFormat() builds the header and lines (§4.2).
  3. Header and detail payloads are stored in the export log for traceability.
  4. saveVoucherLocally() writes voucher_headers / voucher_lines (postedToOracle=false).
  5. writeToOracle():
    • INSERT DT_PURCHASE_HEADERS_IVCPRO (RETURNING PBH_ID) and DT_PURCHASE_LINES_IVCPRO, then COMMIT;
    • BEGIN copy_pb_ivcpro_fas(:pbh_id, :out_status, :out_voucher); COMMIT; END;.
  6. Local voucher updated (postedToOracle, oraclePbhId, oracleCopyStatus, vcVoucherNo); log → success.

On error: log failed with the step number, and the job throws so Graphile retries (FAS_EXPORT_MAX_ATTEMPTS, default 3). writeToDataLayer() plus the Airflow fas_writeback_pipeline is the older, deprecated route.

3.11 InterCompany code path​

  • Sync: sync-fas-transaction-today-cron (upsert today's rows) and sync-fas-transaction-full-cron (truncate + reload), both reading production.in_fas_dt_transaction.
  • Auto-ignore: auto-ignore-intercompany-cron (today mode at :00/:30, full mode at 03:10) runs scripts/auto-ignore-intercompany.ts.
  • Controller: controllers/intercompany.controller.ts handles pending transactions (filtered and paginated in memory), journal details, COA options, conversion rate, and the contra lifecycle.
  • Export: services/intercompany-export.service.ts inserts into DT_INTERCOMP_HEADERS/LINES_IVCPRO and calls copy_ji_ivcpro_fas. Runs synchronously inside the approve request, unlike invoice export.

3.12 Bank reconciliation code path​

  1. POST /api/bank-reconciliation/upload:
    • parse (§4.4);
    • file-hash duplicate check;
    • create batch;
    • bulk-insert bank_transactions.
  2. performAutomatedMatching() runs fire-and-forget in the API process. Phases 1–3 each write bank_reconciliation_matches and _match_items plus an audit log.
  3. Batch → completed / failed.
  4. The UI polls every 5s.

3.13 Background jobs (worker.ts)​

TaskTriggerPurpose
extract-documentQueued on upload / re-extractSend pages to n8n extractor
create-document-set-cronCron every minute + self-re-enqueue every 20sAuto-create doc sets from the inbox
create-document-setQueued on reprocessSame purpose, event-driven
refresh-inbox-viewCron every minute + about every 15sRefresh mv_inbox_conversations
refresh-approval-list-viewCron every minute + about every 10sRefresh approval list MV
send-emailQueuedNodemailer + email_send_logs
export-to-fasQueued on final approval / retryOracle posting
sync-commitments-cron:00/:30, hours 6–23Commitments (upsert); activations (TRUNCATE + reload)
sync-fas-cron03:00, 07:00, 10:00, 12:25, 14:00, 16:2515 FAS tables, mostly TRUNCATE + reload
sync-fas-transaction-today-cron:25/:55, hours 6–23Today's FAS transactions (upsert)
sync-fas-transaction-full-cron03:00Full FAS transactions
auto-ignore-intercompany-cron:00/:30 and 03:10Auto-ignore contras

The code comments describe these times as Jakarta time. Graphile's crontab runs on the worker's clock, so that holds only if the container's timezone is set to Jakarta.

3.14 Data pipeline (two hops)​

Oracle / PRIS / MMIS / SAP B1 / SharePoint
→ Airflow
oracle_daily: 02:00 / 11:50 / 15:50 (Mon–Fri)
oracle_transaction: :15/:45
sapb1: 03:00
sharepoint: externally triggered
→ Data Layer PG (production.in_* tables; views: commitments, activations)
→ Graphile sync crons (TRUNCATE + row-by-row INSERT, or upsert)
→ Main PG (fas_*, commitments, activations, …)
→ App and n8n read from here

In the reverse direction, finpro_daily_pipeline_2am copies the app's tables (doc sets, approvals, contras, bank recon, users) back into the data layer as in_finpro_* for reporting.

3.15 Frontend flow​

  • Routes: /dashboard (document sets), /dashboard/inbox, /dashboard/document-sets/:id, /dashboard/approvals, /dashboard/intercom-dashboard, /bank-reconciliation/*, /dashboard/admin/*, and the public /approval-preview/:token.
  • The document set detail page is a three-step wizard: Validation → Matching → Voucher. The step is derived from status: matching → Matching; ready, in_approval or approved → Voucher.
  • Data access uses TanStack Query and shared/lib/api-client.ts, with the JWT in localStorage. Extraction status arrives via SSE with polling as a fallback.

4. Deep dives​

4.1 Authentication and access model​

AspectImplementationFile
LoginUsername + password (bcrypt, 10 rounds) → JWT {id, email, name, role}, expiry 6hauth.ts, auth.controller.ts
SecretJWT_SECRET, with fallback "your-secret-key-change-in-production" if unsetauth.ts
Token transportAuthorization: Bearer, or ?token= query param (used for file/SSE-style links)auth.middleware.ts
Session stateStateless. Claims are trusted as-is, with no DB lookup per request. Role changes or user deletion take effect only after the token expires.requireAuth
RegistrationPOST /api/auth/register is public and validates the body with insertUserSchema, which includes roleauth.controller.register
User adminPOST /api/users checks admin role and canAssignRole() (ap-admin → AP roles; fin-admin → finance roles)user.controller.ts
PermissionsCasbin for intercom:* routes; inline role checks elsewhere; company scoping via user_companiescasbin-*, controllers
WebhooksShared static token in header x-webhook-tokenrequireWebhookAuth
Public endpoints/api/approval-tokens/*, /api/extraction-events (SSE), /api/documents/:id/file (also serves email attachments by ID)routes.ts
n8n exposureNginx routes /webhook/ and /rest/ to n8n (rate-limited)nginx/conf.d/workflow.conf.template

4.2 FAS posting: header and line mapping (mapToFasFormat)​

Header → DT_PURCHASE_HEADERS_IVCPRO

FAS columnSource
vc_cont_codedocument_sets.fas_contract
dt_voucher_dateExport date (today)
vc_vat_noTax slip code, or -
vc_comp_codefas_mk_inter_company_ou.vcCompCode for the company
vc_supp_codefas_mst_contract.vcSuppCode
vc_bill_noInvoice number
dt_bill_datedocument_sets.bill_date (= when matching started)
dt_due_datebill_date + fas_mst_contract.nuCreditDays
order_typeFrom commitment.orderType → P / S / C
nu_currency_code, nu_conv_factor_dfltFAS currency code; nuRbconvFactor at bill date
nu_f_currency_crAP credit (transaction currency)
nu_amount_crAP credit converted to USD with 1 / nuRbconvFactor
vc_auth_code, created_by, modified_byfasUserCode of the approval requester (variable is named approverUser)
vc_account_unitHard-coded N005
payment_status_codeHard-coded H
vc_advance_typeHard-coded 01
dt_vat_dateTax slip date, else bill date
vc_mainacc/subacc1/subacc2From the AP voucher line account string
vc_cont_bankacctnoInvoice OCR bank account no. (code has TODO: more reliable way)
vc_cont_vatregnoTax receipt vendor NPWP, else contract VAT reg, else -
compcodeCompany code

Lines → DT_PURCHASE_LINES_IVCPRO. Built from every voucher entry. Each has a serial number, Dr/Cr in transaction currency, and Dr/Cr converted to USD with 1 / rate; account split from main-sub1-sub2. Then details.pop() drops the last element, on the assumption that it is the AP line. The header itself locates AP by entryType.

4.3 Medical claim vouchers​

Triggered when isMedicalInvoice(invoiceData) is true (based on invoice type) and activations exist (medical-voucher-generator.service.ts, medical-claims.helper.ts):

  • Expense account comes from the category parsed out of activation notes (Medical Category: …): General → 451218, Hospital → 451219, Examin → 451220.
  • Sub-account is E + the 7-digit employee number taken from the item code.
  • Department / cost center is parsed from the end of the activation detail description.
  • In the UI, the tax receipt tab is hidden for hospital-type invoices.

4.4 Bank reconciliation internals​

  • Bank detection: by sheet name in utils/bank-statement-extractor.ts:

    • AccountStatement_*;
    • DD_ONLINE_STATEMENT;
    • Worksheet;
    • transaction_inquiry_report;
    • transaction_history.

    These cover BNI / BRI / BSI / Mandiri / Niaga. Header and footer metadata go through Bedrock (bank-statement-llm-extraction.service.ts).

  • FAS candidate set: fas_dt_transaction rows that:

    • belong to the batch entity;
    • fall in the voucher-date range of the batch month;
    • post to the bank account's COA from fas_mst_bank (main / sub1 / sub2; sub1 '0' is mapped to '00000');
    • are not already used in a match.
  • Phase 1: amount match only when exactly one FAS entry has that amount (tolerance < 0.01). Ambiguous amounts are left for later phases.

  • Phase 2: the bank description contains the FAS voucher number.

  • Phase 3: one batch prompt to the LLM returns matches plus a bankCharges[] list (heuristics in the prompt: small amounts, words like fee / charge / admin).

4.5 Matching gate (client-side)​

MatchingSection.tsx → canProceedToFAS enables Mark Ready only when all of these hold:

  • status is not already ready, in_approval or approved;
  • matching has completed;
  • every issue with isError && severity ∈ {error, warning} has a non-null override.

The server's mark-ready checks only the current status (matching).

4.6 Airflow incremental extraction​

oracle_daily/extract/extract_incremental.py picks a timestamp column per Oracle table, in priority order: MODIFICATION_DATE, DT_POSTED, UPDATE_DATE, UPDATED, DOCDATE. The lower bound (watermark) is derived from the names of previous local extract folders (oracle_extracts_YYYYMMDDTHHMMSS), capped at 5 days. Tables without a timestamp column fall back to full copy. Rows deleted in Oracle are not captured by this approach.


5. Findings: risks and tech debt​

Severity is the analyst's judgment. "Verify" means the code suggests the issue but it should be confirmed against runtime, production logs or configuration.

5.1 Security​

#Sev.FindingWhere
S1CriticalPublic POST /api/auth/register accepts role in the body (via insertUserSchema), so anyone who can reach the API could self-register as admin. Nginx does not block it. Verify on the deployed environment.routes.ts, auth.controller.ts, schema.ts
S2HighGET /api/documents/:id/file has no auth and also serves email attachments by ID. IDs are UUIDs, which lowers but does not remove the risk.routes.ts, documents.controller.serveDocumentFile
S3HighJWT secret has a hard-coded fallback; token accepted via query string, so it can leak into logs and referrers.auth.ts, auth.middleware.ts
S4MediumClaims trusted without a DB check, so a removed or demoted user keeps access for up to 6h.requireAuth
S5Mediummulter runs before requireAuth on upload routes, so files hit disk before authentication.routes.ts
S6MediumSSE /api/extraction-events is unauthenticated and broadcasts all extraction events.webhooks.controller.getExtractionEvents
S7Mediumn8n /webhook/* is publicly routed. Verify that the n8n webhook nodes require auth; otherwise anyone can trigger paid Bedrock calls.nginx/conf.d/workflow.conf.template
S8LowCasbin coverage is partial, and ap-approver is missing from the policy. Defaults are hard-coded: a Bedrock ARN containing an AWS account ID, and an APP_URL fallback.casbin-policy.csv, services

5.2 Workflow correctness​

#Sev.FindingWhere
W1HighPATCH /api/document-sets/:id/status accepts any status, including approved, for any non-viewer. This bypasses matching and approval.document-sets.controller.updateDocumentSetStatus
W2Highmark-ready has no server-side check that issues are overridden; gating is UI-only (§4.5).document-sets.controller.markReady
W3High"Gate 1" contradicts its comment. For required types, processAttachment triggers extraction immediately without setting EXTRACTING. checkAllCategorizedAndTriggerExtraction runs only on the other path and re-triggers attachments still pending, which can cause double extraction.webhooks.controller.processAttachment
W4Mediumn8n triggers are fire-and-forget (no queue, no retry). If n8n is down, attachments stay pending until someone reprocesses manually.email-document-processing.helper.ts
W5MediumThree doc-set creation paths with different rules: the cron makes the tax receipt optional, the manual inbox path requires it, and the reprocess worker is a third copy.workers/create-document-set*.ts, inbox.controller.ts
W6MediumOn rejection the doc set stays in_approval. Send-back hard-deletes approval, items and tokens, so history survives only in audit_logs.approvals.controller.ts
W7MediumNo DB transactions on multi-step writes (there is an explicit TODO in approval processing).controllers / workers
W8Lowmanual-match, resolve-action and confirm-action routes return 501.document-sets.controller.ts
W9LowServer calls itself over HTTP (API_URL) to validate commitment and GR references.updateCommitmentReference, updateGrReference

5.3 Money paths (FAS)​

#Sev.FindingWhere
M1Highcopy_pb_ivcpro_fas errors are swallowed. The export is marked success, with only oracleCopyStatus = PROC_ERROR… recording the failure.fas-export.service.writeToOracle
M2HighRetries after the Oracle commit re-insert staging rows. No idempotency key or dedupe on vc_bill_no / doc set was found.fas-export.service.ts
M3HighFAS base amounts (nu_amount_*) are always converted to USD, ignoring the voucher's baseCurr, which the generator computes in the book currency derived from vcCompCode. Verify against how FAS expects base currency for IDR-book companies.mapToFasFormat vs voucher-generator.service.ts
M4Mediumdetails.pop() assumes the AP line is last. Edited or reordered voucher lines could drop the wrong line.mapToFasFormat
M5MediumDinero({ amount: parseFloat(x) * 100 }) can produce non-integer cents (e.g. 0.29*100). Dinero v1 rejects non-integers, so some amounts could fail export.mapToFasFormat
M6MediumHard-coded fallback FX rate of 15000 (IDR → invoice currency) when no rate is found.voucher-generator.service.ts
M7Lowbill_date = matching start date, which drives FX, due date and dt_bill_date. Confirm this is the intended business rule rather than the invoice date.startMatching
M8InfoNo flow syncs FAS posting, cancellation or reversal status back onto voucher_headers after the stored procedure returns.—
Currency-conversion service (post-snapshot)

services/currency-conversion.service.ts (added 2026-03-11) centralises invoice↔book-currency conversion and is wired into voucher-generator.service.ts. It has not been wired into mapToFasFormat, so M3 and M5 still stand as of the analysed revision.

5.4 Integration and data sync​

#Sev.FindingWhere
D1HighMost syncs TRUNCATE then insert row by row without a transaction. Reference tables (activations, FAS contracts, currency, account rules, …) are empty or partial during the sync, so matching or voucher generation in that window can fail.scripts/sync-fas-data.ts, sync-commitments-cron.ts
D2MediumRedundant two-hop pipeline (Airflow → data layer → worker copy). At 03:00, sync-fas-cron already includes a full transaction sync and sync-fas-transaction-full-cron runs too.worker.ts
D3MediumThe Airflow watermark depends on local folder names (prune_old_extracts exists), and deletes aren't captured.extract_incremental.py
D4MediumThe n8n 3-way-match reads the app DB directly; rules are static JS plus prompts inside n8n, so it is schema-coupled and hard to version or test.n8n workflow
D5LowThe race condition is patched with MV refresh loops (10–15s) and a 20s polling cron.workers

5.5 Scalability and operability​

#Sev.FindingWhere
O1MediumBank-recon matching runs in the API process, fire-and-forget. A restart leaves batches stuck in processing.bank-reconciliation.controller.uploadBankStatement
O2MediumSSE uses an in-process EventEmitter, which breaks with more than one API replica.utils/sse.ts
O3MediumIn-memory filtering and pagination (intercompany slice()); the client fetches activations/all?limit=5000.intercompany.controller.ts, api-client.ts
O4LowInterCompany export runs synchronously in the HTTP request.intercompany.controller.approveContraEntry
O5Lowstorage.ts (~238 KB) and the large controllers concentrate logic; test coverage is thin.—

5.6 Data model​

#Finding
DM1Exactly three document types, each with a typed table. A combined PDF (BAST + Faktur + Invoice in one file) is forced into a single type.
DM2voucher_entries and matching_summary are JSONB blobs on document_sets. There is no per-line history except the audit log.
DM3Legacy dual fields (fileName/fileNames, extractedData JSON plus typed tables) are still read on several paths.

6. Mapping to V2 workstreams​

V2 workstreamRelevant current stateFindings to address
PRIS integration (vendor invoice + status sync)PRIS reaches the app only as read-only reference data (commitments, activations/BAST) through Airflow → data layer → TRUNCATE sync. No write-back or status sync to PRIS exists.D1, D2, M8
Data pipeline fix (CDC / Kafka, incremental)Timestamp watermark from folder names; full replace in workers; two hopsD1, D2, D3, D5
Document Set flow / status / logsFive-state machine with a free-form status PATCH; three creation paths; status history only for conversationsW1, W3, W4, W5, W6, W7, DM1
3-way matching + AI split (extract vs reasoning)Extraction and classification in n8n prompts; rules 1–3, 8, 9 deterministic JS in n8n; rules 4–7 LLM; n8n reads DB directlyD4, W2, DM1
Approval UXEmail-token-only sequential approval; rejection leaves the set in in_approval; send-back deletes historyW6, S1–S4
Oracle EBS integration (incl. journal cancel status sync)Integration seams: fas-export.service.writeToOracle (DT_PURCHASE_*_IVCPRO + copy_pb_ivcpro_fas) and intercompany-export.service (DT_INTERCOMP_*_IVCPRO + copy_ji_ivcpro_fas). No status sync back.M1–M8
User convenienceInbox and approval lists on MV polling; client-side heavy listsO2, O3, D5

7. Not yet verified​

  • Contents of mv_inbox_conversations and the approval-list MV beyond their joins (attachment, email, vendor and duplicate-invoice stats).
  • copy_pb_ivcpro_fas / copy_ji_ivcpro_fas behaviour inside Oracle (validation, duplicate handling).
  • Whether n8n webhook nodes enforce auth (S7) and whether /api/auth/register is reachable in production (S1).
  • Per-bank column parsing details and the full LLM matching prompt.
  • Client-side behaviour of the InterCompany and Admin modules beyond routing.
  • Drizzle migrations vs schema.ts drift.

8. Repository reference​

Documentation already in the repo​

The repository is doc-rich; prefer these over re-deriving facts:

FileContents
README.mdMain project README (~31 KB) — structure, setup, module notes
InvoicePro_PRD.mdProduct requirements (~22 KB)
CHANGELOG.mdPer-change history (~32 KB, newest first, reverse-chronological)
CLAUDE.mdAgent/dev conventions — structure, build/test, security rules, three-phase workflow
docs/HANDOVER.mdHandover notes (~66 KB)
docs/USE_CASE_INVOICE_PROCESSING.mdInvoice-processing use case
docs/USE_CASE_INTERCOMPANY.mdInterCompany use case
docs/USE_CASE_BANK_RECONCILIATION.mdBank-reconciliation use case
docs/WORKER_JOBS.mdEvery Graphile worker job: triggers, payloads, flows, monitoring
docs/AUTO_IGNORE_SCHEDULING.md, docs/TESTING_AUTO_IGNORE.mdAuto-ignore scheduling and tests
docs/EC2_SETUP_GUIDE.md, docs/USER_COUNTRY_SEEDING.mdDeployment and seeding
docs/*.drawioFlow diagrams (document-set creation/worker flow, classification status, contract query)

Build and test​

# Server
cd server && npm run dev # Dev with hot reload
cd server && npm run build # Production build (esbuild)
cd server && npm test # Tests (vitest)
cd server && npm run db:push # Push schema changes
cd server && npm run db:migrate # Run migrations

# Client
cd client && npm run dev # Dev with hot reload (vite)
cd client && npm run build # Production build

# Docker
make docker-dev-build-invoice-pro # Build and start dev services
make docker-dev-down # Stop dev containers

Conventions​

  • Monorepo with two packages: server/ (@finance-pro/server) and client/ (@finance-pro/client).
  • Co-locate unit tests next to source (foo.ts → foo.test.ts); integration/service tests go in server/src/__tests__/.
  • Three-phase workflow: Understand (read + summarise current behaviour) → Plan (before/after diffs with reasoning) → TDD (red-green-refactor).

Source of this page​

  • Code analysis (primary): finance-pro-business-and-code-flow.md, snapshot dated 2026-09-16 — reconciled against the working tree at develop @ 926cae40 (2026-10-02).
  • Repository: emp/invoicepro.
  • Drift at the analysed revision: new files server/src/lib/oracle-test-client.ts and server/src/scripts/test-oracle-insert.ts (oracle test, 2026-10-02), sync-fas-data.ts timestamptz casts (issue #262, 2026-09-17), storage.ts period-filter fix (issue #265, 2026-09-30), and approval voucher-display fixes in client/src/pages/approvals/ (issue #264, 2026-09-25). None of them change the flows or findings above.
  • The GitLab REST API token configured in the gitlab-code-fetcher skill was expired at the time of writing; the repository was inspected from a local clone of the same remote.