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.
- 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), version1.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
- System overview
- Business flow
- Code flow
- Deep dives
- Findings: risks and tech debt
- Mapping to V2 workstreams
- Not yet verified
- 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 meaning | Key powers |
|---|---|---|
admin | Super admin | Everything |
ap-admin | AP administrator | Doc sets, users, approval config |
ap-user | AP clerk | Process doc sets, request approval |
ap-approver | AP approver | Approves via email token; can also create approval requests |
fin-admin / fin-user / fin-approver | Finance (InterCompany) | fin-user edits contra entries; fin-approver / fin-admin approve |
viewer | Read-only | Blocked from mutations by inline checks |
| Bank-recon users | Grant table bank_reconciliation_permissions | Access to bank recon (admins always have it) |
SYSTEM / AUTOMATION_USER | Automation identity | Owns 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
| Concept | What it is | Source |
|---|---|---|
| Commitment | PO / SO / Contract (approved value, vendor, account codes) | production.commitments view (MMIS / PRIS / …) |
| Activation | Proof of delivery: BAST (PRIS), MRR (SAP B1), etc. Keyed by DO/GR no. + PO no. | production.activations view |
| FAS Contract | FAS contract master: supplier code, company, credit days, validity | in_fas_mst_contract |
| Account rules | COA combinations (main / sub1 / sub2), descriptions, IC flag | in_fas_mst_v_account_rule |
| Currency conversion | nuRbconvFactor per currency, company and date | in_fas_currency_conv |
| FAS transactions | Posted journals (InterCompany, Bank Recon) | in_fas_dt_transaction |
| Bank master | Bank account to COA mapping | in_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:
-
Intake. Emails are threaded into conversations via the
ReferencesandIn-Reply-Toheaders. The vendor is identified by sender address againstvendor_emails. Unknown senders are still ingested, but a doc set is only auto-created once some email in the thread has avendor_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. -
Document types. Exactly three types drive the process: invoice, tax receipt (Faktur Pajak) and goods receipt. Everything else is
other(preview only). -
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). -
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. -
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:Rule Check Notes 3WM-00001 PO/SO/Contract no. consistent across contract, GR and invoice Fuzzy, 85% similarity to tolerate OCR typos 3WM-00002 GR numbers on invoice match the selected/uploaded GRs 3WM-00003 Vendor name fuzzy match across contract, GR, invoice, tax receipt Tax receipt skipped if overridden 3WM-00004–07 Line items: invoice vs GR vs commitment (description, qty, unit price, amount) LLM-based (Bedrock); count mismatch = error 3WM-00008 Invoice value = Σ GR payment values, and ≤ remaining contract amount 3WM-00009 GR delivery date within contract validity Warning Rule ID driftThe n8n workflow JSON at this revision defines
3WM-00001,-00002,-00003,-00004,-00007,-00008,-00009. Rules-00005and-00006are not declared as separate IDs; the line-item family runs as one block.Each issue carries
severity(error/warning/info),isError, and anoverridethat is nullable. Gate to Mark Ready (client-side): every issue withisError = trueand severityerrororwarningmust have an override with a reason of at least 10 characters. The tax receipt can be overridden separately. -
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:
040Normal: tax expense Dr only.030WAPU: tax expense Dr plus tax payable213105Cr.070Exempt: 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.
- Tonga
- AP (Cr, balancing):
211111(IDR) or211112(USD), sub100000, sub2 = supplier code from the FAS contract. - Medical invoices use a separate generator (§4.3).
-
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 withminAmount ≤ 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.
- Approvers: passed in explicitly or auto-resolved. Auto-resolution converts the invoice subtotal (before tax) to USD (
-
Posting. Background job with retries. Visible in
invoice_fas_export_logsandvoucher_headers(postedToOracle,oracleCopyStatus,vcVoucherNo).
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:
| Entity | Statuses |
|---|---|
| Conversation | new → filecat → fileext → done, plus confirm (duplicate) and error (failed attachment or missing invoice/GR). Full history in status_history. |
| Attachment | pending → extracting → completed / failed |
| Document | extraction_status: pending / processing / completed / failed |
| Document set | validating → matching → ready → in_approval → approved |
| Approval + items | review_approvers → pending → approved / rejected |
| FAS export log | pending → 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
| Process | Entry | Responsibility |
|---|---|---|
| API | server/src/index.ts | REST, webhooks, SSE. Creates makeWorkerUtils to enqueue jobs. Also runs bank-recon matching in-process. |
| Worker | server/src/worker.ts | Background tasks and crons (WORKER_CONCURRENCY, default 5) |
| n8n | docker | IMAP polling, AI classification and extraction, 3-way matching |
| Airflow | separate compose | Oracle / SAP / SharePoint → data layer |
| Nginx | nginx/ | TLS, routing to SPA / API / n8n / Airflow |
3.3 Request lifecycle (API)
express.json/urlencoded.- Request logger (logs
/apicalls with truncated response JSON). - Route-level middleware:
requireAuth: JWT fromAuthorization: Beareror?token=query param. The decoded claims becomereq.userwith no DB lookup.requirePermission(obj, act): Casbin, used only on some intercompany routes.requireBankReconAccess: admin roles, or a row inbank_reconciliation_permissions.requireAdmin.requireWebhookAuth: headerx-webhook-tokenequalsWEBHOOK_AUTH_TOKEN.multerupload: to disk underuploads/, or to memory for bank files.
- 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"):
- A raw SQL query selects up to 10 eligible conversations (criteria in §2.3).
- For each one:
- duplicate-invoice check, otherwise insert
document_sets; - insert
documentsfor the latest invoice, latest tax receipt and all GRs; saveExtractedData()fillsinvoice_data/tax_receipt_data/goods_receipt_data/invoice_line_items;- audit logs;
documentSetHandled = true, status DONE.
- duplicate-invoice check, otherwise insert
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
POST /api/documents/upload(multer).- Existing invoice and tax receipt documents of the same type are soft-deleted.
- The file is stored.
- An
extract-documentjob is queued (maxAttempts3). - The worker downloads from S3, converts PDFs to images (max 4 files × 4 pages, resized), and POSTs them to
EXTRACTION_SERVICE_URL_<TYPE>. - n8n calls back
POST /api/webhooks/extraction-complete. - The callback maps fields into the typed tables, replaces line items, writes a field-level audit diff, and broadcasts over SSE (
/api/extraction-events). - 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:
- Allowed only from
matching. No server-side check of unresolved issues. fasContract = contractPoSoNumber.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()orgenerateVoucherEntries().
- Status
ready, and the result goes todocument_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
| Step | Endpoint | Effect |
|---|---|---|
| Resolve | GET /api/approval-config/resolve/:documentSetId | Tier and approvers by USD amount |
| Create | POST /api/approvals | Approval + items = review_approvers; doc set = in_approval |
| Send | POST /api/approvals/:id/send-for-approval | Approval = pending; first item = pending; token row; email_send_logs row; send-email job (Nodemailer, 3 attempts) |
| Approver view | GET /api/approval-tokens/:token (public) | SPA route /approval-preview/:token |
| Act | POST /api/approval-tokens/:token/process | Item approved/rejected; token usedAt; calculateApprovalStatus(). Approved with items left: next item pending + email. All approved: doc set approved + enqueueFasExport(). |
| Admin | resend / restart / reassign / send-back-to-processing / retry-fas-export | Send-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:
- Export log →
exporting. mapToFasFormat()builds the header and lines (§4.2).- Header and detail payloads are stored in the export log for traceability.
saveVoucherLocally()writesvoucher_headers/voucher_lines(postedToOracle=false).writeToOracle():- INSERT
DT_PURCHASE_HEADERS_IVCPRO(RETURNING PBH_ID) andDT_PURCHASE_LINES_IVCPRO, then COMMIT; BEGIN copy_pb_ivcpro_fas(:pbh_id, :out_status, :out_voucher); COMMIT; END;.
- INSERT
- 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) andsync-fas-transaction-full-cron(truncate + reload), both readingproduction.in_fas_dt_transaction. - Auto-ignore:
auto-ignore-intercompany-cron(today mode at :00/:30, full mode at 03:10) runsscripts/auto-ignore-intercompany.ts. - Controller:
controllers/intercompany.controller.tshandles pending transactions (filtered and paginated in memory), journal details, COA options, conversion rate, and the contra lifecycle. - Export:
services/intercompany-export.service.tsinserts intoDT_INTERCOMP_HEADERS/LINES_IVCPROand callscopy_ji_ivcpro_fas. Runs synchronously inside the approve request, unlike invoice export.
3.12 Bank reconciliation code path
POST /api/bank-reconciliation/upload:- parse (§4.4);
- file-hash duplicate check;
- create batch;
- bulk-insert
bank_transactions.
performAutomatedMatching()runs fire-and-forget in the API process. Phases 1–3 each writebank_reconciliation_matchesand_match_itemsplus an audit log.- Batch →
completed/failed. - The UI polls every 5s.
3.13 Background jobs (worker.ts)
| Task | Trigger | Purpose |
|---|---|---|
extract-document | Queued on upload / re-extract | Send pages to n8n extractor |
create-document-set-cron | Cron every minute + self-re-enqueue every 20s | Auto-create doc sets from the inbox |
create-document-set | Queued on reprocess | Same purpose, event-driven |
refresh-inbox-view | Cron every minute + about every 15s | Refresh mv_inbox_conversations |
refresh-approval-list-view | Cron every minute + about every 10s | Refresh approval list MV |
send-email | Queued | Nodemailer + email_send_logs |
export-to-fas | Queued on final approval / retry | Oracle posting |
sync-commitments-cron | :00/:30, hours 6–23 | Commitments (upsert); activations (TRUNCATE + reload) |
sync-fas-cron | 03:00, 07:00, 10:00, 12:25, 14:00, 16:25 | 15 FAS tables, mostly TRUNCATE + reload |
sync-fas-transaction-today-cron | :25/:55, hours 6–23 | Today's FAS transactions (upsert) |
sync-fas-transaction-full-cron | 03:00 | Full FAS transactions |
auto-ignore-intercompany-cron | :00/:30 and 03:10 | Auto-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_approvalorapproved→ Voucher. - Data access uses TanStack Query and
shared/lib/api-client.ts, with the JWT inlocalStorage. Extraction status arrives via SSE with polling as a fallback.
4. Deep dives
4.1 Authentication and access model
| Aspect | Implementation | File |
|---|---|---|
| Login | Username + password (bcrypt, 10 rounds) → JWT {id, email, name, role}, expiry 6h | auth.ts, auth.controller.ts |
| Secret | JWT_SECRET, with fallback "your-secret-key-change-in-production" if unset | auth.ts |
| Token transport | Authorization: Bearer, or ?token= query param (used for file/SSE-style links) | auth.middleware.ts |
| Session state | Stateless. Claims are trusted as-is, with no DB lookup per request. Role changes or user deletion take effect only after the token expires. | requireAuth |
| Registration | POST /api/auth/register is public and validates the body with insertUserSchema, which includes role | auth.controller.register |
| User admin | POST /api/users checks admin role and canAssignRole() (ap-admin → AP roles; fin-admin → finance roles) | user.controller.ts |
| Permissions | Casbin for intercom:* routes; inline role checks elsewhere; company scoping via user_companies | casbin-*, controllers |
| Webhooks | Shared static token in header x-webhook-token | requireWebhookAuth |
| Public endpoints | /api/approval-tokens/*, /api/extraction-events (SSE), /api/documents/:id/file (also serves email attachments by ID) | routes.ts |
| n8n exposure | Nginx 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 column | Source |
|---|---|
vc_cont_code | document_sets.fas_contract |
dt_voucher_date | Export date (today) |
vc_vat_no | Tax slip code, or - |
vc_comp_code | fas_mk_inter_company_ou.vcCompCode for the company |
vc_supp_code | fas_mst_contract.vcSuppCode |
vc_bill_no | Invoice number |
dt_bill_date | document_sets.bill_date (= when matching started) |
dt_due_date | bill_date + fas_mst_contract.nuCreditDays |
order_type | From commitment.orderType → P / S / C |
nu_currency_code, nu_conv_factor_dflt | FAS currency code; nuRbconvFactor at bill date |
nu_f_currency_cr | AP credit (transaction currency) |
nu_amount_cr | AP credit converted to USD with 1 / nuRbconvFactor |
vc_auth_code, created_by, modified_by | fasUserCode of the approval requester (variable is named approverUser) |
vc_account_unit | Hard-coded N005 |
payment_status_code | Hard-coded H |
vc_advance_type | Hard-coded 01 |
dt_vat_date | Tax slip date, else bill date |
vc_mainacc/subacc1/subacc2 | From the AP voucher line account string |
vc_cont_bankacctno | Invoice OCR bank account no. (code has TODO: more reliable way) |
vc_cont_vatregno | Tax receipt vendor NPWP, else contract VAT reg, else - |
compcode | Company 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_transactionrows 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_approvalorapproved; - matching has completed;
- every issue with
isError && severity ∈ {error, warning}has a non-nulloverride.
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. | Finding | Where |
|---|---|---|---|
| S1 | Critical | Public 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 |
| S2 | High | GET /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 |
| S3 | High | JWT secret has a hard-coded fallback; token accepted via query string, so it can leak into logs and referrers. | auth.ts, auth.middleware.ts |
| S4 | Medium | Claims trusted without a DB check, so a removed or demoted user keeps access for up to 6h. | requireAuth |
| S5 | Medium | multer runs before requireAuth on upload routes, so files hit disk before authentication. | routes.ts |
| S6 | Medium | SSE /api/extraction-events is unauthenticated and broadcasts all extraction events. | webhooks.controller.getExtractionEvents |
| S7 | Medium | n8n /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 |
| S8 | Low | Casbin 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. | Finding | Where |
|---|---|---|---|
| W1 | High | PATCH /api/document-sets/:id/status accepts any status, including approved, for any non-viewer. This bypasses matching and approval. | document-sets.controller.updateDocumentSetStatus |
| W2 | High | mark-ready has no server-side check that issues are overridden; gating is UI-only (§4.5). | document-sets.controller.markReady |
| W3 | High | "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 |
| W4 | Medium | n8n 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 |
| W5 | Medium | Three 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 |
| W6 | Medium | On 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 |
| W7 | Medium | No DB transactions on multi-step writes (there is an explicit TODO in approval processing). | controllers / workers |
| W8 | Low | manual-match, resolve-action and confirm-action routes return 501. | document-sets.controller.ts |
| W9 | Low | Server calls itself over HTTP (API_URL) to validate commitment and GR references. | updateCommitmentReference, updateGrReference |
5.3 Money paths (FAS)
| # | Sev. | Finding | Where |
|---|---|---|---|
| M1 | High | copy_pb_ivcpro_fas errors are swallowed. The export is marked success, with only oracleCopyStatus = PROC_ERROR… recording the failure. | fas-export.service.writeToOracle |
| M2 | High | Retries 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 |
| M3 | High | FAS 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 |
| M4 | Medium | details.pop() assumes the AP line is last. Edited or reordered voucher lines could drop the wrong line. | mapToFasFormat |
| M5 | Medium | Dinero({ 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 |
| M6 | Medium | Hard-coded fallback FX rate of 15000 (IDR → invoice currency) when no rate is found. | voucher-generator.service.ts |
| M7 | Low | bill_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 |
| M8 | Info | No flow syncs FAS posting, cancellation or reversal status back onto voucher_headers after the stored procedure returns. | — |
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. | Finding | Where |
|---|---|---|---|
| D1 | High | Most 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 |
| D2 | Medium | Redundant 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 |
| D3 | Medium | The Airflow watermark depends on local folder names (prune_old_extracts exists), and deletes aren't captured. | extract_incremental.py |
| D4 | Medium | The 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 |
| D5 | Low | The race condition is patched with MV refresh loops (10–15s) and a 20s polling cron. | workers |
5.5 Scalability and operability
| # | Sev. | Finding | Where |
|---|---|---|---|
| O1 | Medium | Bank-recon matching runs in the API process, fire-and-forget. A restart leaves batches stuck in processing. | bank-reconciliation.controller.uploadBankStatement |
| O2 | Medium | SSE uses an in-process EventEmitter, which breaks with more than one API replica. | utils/sse.ts |
| O3 | Medium | In-memory filtering and pagination (intercompany slice()); the client fetches activations/all?limit=5000. | intercompany.controller.ts, api-client.ts |
| O4 | Low | InterCompany export runs synchronously in the HTTP request. | intercompany.controller.approveContraEntry |
| O5 | Low | storage.ts (~238 KB) and the large controllers concentrate logic; test coverage is thin. | — |
5.6 Data model
| # | Finding |
|---|---|
| DM1 | Exactly three document types, each with a typed table. A combined PDF (BAST + Faktur + Invoice in one file) is forced into a single type. |
| DM2 | voucher_entries and matching_summary are JSONB blobs on document_sets. There is no per-line history except the audit log. |
| DM3 | Legacy dual fields (fileName/fileNames, extractedData JSON plus typed tables) are still read on several paths. |
6. Mapping to V2 workstreams
| V2 workstream | Relevant current state | Findings 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 hops | D1, D2, D3, D5 |
| Document Set flow / status / logs | Five-state machine with a free-form status PATCH; three creation paths; status history only for conversations | W1, 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 directly | D4, W2, DM1 |
| Approval UX | Email-token-only sequential approval; rejection leaves the set in in_approval; send-back deletes history | W6, 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 convenience | Inbox and approval lists on MV polling; client-side heavy lists | O2, O3, D5 |
7. Not yet verified
- Contents of
mv_inbox_conversationsand the approval-list MV beyond their joins (attachment, email, vendor and duplicate-invoice stats). copy_pb_ivcpro_fas/copy_ji_ivcpro_fasbehaviour inside Oracle (validation, duplicate handling).- Whether n8n webhook nodes enforce auth (S7) and whether
/api/auth/registeris 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.tsdrift.
8. Repository reference
Documentation already in the repo
The repository is doc-rich; prefer these over re-deriving facts:
| File | Contents |
|---|---|
README.md | Main project README (~31 KB) — structure, setup, module notes |
InvoicePro_PRD.md | Product requirements (~22 KB) |
CHANGELOG.md | Per-change history (~32 KB, newest first, reverse-chronological) |
CLAUDE.md | Agent/dev conventions — structure, build/test, security rules, three-phase workflow |
docs/HANDOVER.md | Handover notes (~66 KB) |
docs/USE_CASE_INVOICE_PROCESSING.md | Invoice-processing use case |
docs/USE_CASE_INTERCOMPANY.md | InterCompany use case |
docs/USE_CASE_BANK_RECONCILIATION.md | Bank-reconciliation use case |
docs/WORKER_JOBS.md | Every Graphile worker job: triggers, payloads, flows, monitoring |
docs/AUTO_IGNORE_SCHEDULING.md, docs/TESTING_AUTO_IGNORE.md | Auto-ignore scheduling and tests |
docs/EC2_SETUP_GUIDE.md, docs/USER_COUNTRY_SEEDING.md | Deployment and seeding |
docs/*.drawio | Flow 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) andclient/(@finance-pro/client). - Co-locate unit tests next to source (
foo.ts→foo.test.ts); integration/service tests go inserver/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 atdevelop@926cae40(2026-10-02). - Repository:
emp/invoicepro. - Drift at the analysed revision: new files
server/src/lib/oracle-test-client.tsandserver/src/scripts/test-oracle-insert.ts(oracle test, 2026-10-02),sync-fas-data.tstimestamptz casts (issue #262, 2026-09-17),storage.tsperiod-filter fix (issue #265, 2026-09-30), and approval voucher-display fixes inclient/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-fetcherskill was expired at the time of writing; the repository was inspected from a local clone of the same remote.