Skip to content

019fb836 DfxApproval-GSheets durch API-Workflow ersetzen - #4553

Merged
TaprootFreak merged 43 commits into
developfrom
codex/dfxapproval-api-workflow
Aug 14, 2026
Merged

019fb836 DfxApproval-GSheets durch API-Workflow ersetzen#4553
TaprootFreak merged 43 commits into
developfrom
codex/dfxapproval-api-workflow

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

EN:
This pull request replaces the eight live personal DfxApproval Google Sheets with a default-off API workflow.
The minute job generates the six compliance PDFs, rotates blocked cases out of the review window, and completes personal KYC only when the gate is clear.
It stays inert unless KYC_DFX_APPROVAL_WORKFLOW_ENABLED is exactly true and the KYC_DFX_APPROVAL process flag is on.
Every touched production file is pinned at 100 percent coverage.

DE:
Dieser Pull Request ersetzt die acht produktiven persönlichen DfxApproval-Google-Sheets durch einen default-off API-Workflow.
Der Minutenjob erzeugt die sechs Compliance-PDFs, rotiert blockierte Fälle aus dem Review-Fenster und schliesst persönliches KYC nur bei freiem Gate ab.
Ohne KYC_DFX_APPROVAL_WORKFLOW_ENABLED genau true und ohne Process-Flag KYC_DFX_APPROVAL bleibt er inert.
Jede angefasste Produktionsdatei ist auf 100 Prozent Coverage gepinnt.

Details

Replaces the personal DfxApproval sheets (approval, risk flags, GwGFileCover, IdentificationForm, CustomerProfile, RiskProfile, FormA, DfxNameCheck). IdentReport and PersonalNameCheck stay in their existing KYC processes. Organizations are out of scope.

Default-off: KYC_DFX_APPROVAL_WORKFLOW_ENABLED === 'true' and Process.KYC_DFX_APPROVAL. Advisory locks are session-scoped (pg_try_advisory_lock), not transactional. Document generation loads candidate ids, not full user graphs. Permanently not-ready review steps, skipped risk-init accounts and failed CustomerProfile renders bump updated so they do not occupy the oldest-50/500 window. Characters the embedded font cannot encode are substituted and logged once per document.

No public API contract change. Migration adds nullable unique generationKey on kyc_file. Cron inventory, test-reality fakes and load-sites are updated in this PR.

@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

Verification state after the review corrections:

  • 144 tests in 10 suites for the KYC subdomain locally, all 13 GitHub checks green
  • both corrected loads executed against a real PostgreSQL 16 with the pinned driver version: the
    previous versions fail there with TypeError: Cannot read properties of undefined (reading 'id')
    and FOR UPDATE cannot be applied to the nullable side of an outer join, the new ones return the
    locked rows
  • the lock alias case is covered by a test: the driver emits FOR UPDATE OF <alias> unquoted, and a
    camelCase alias fails with relation "kycstep" in FOR UPDATE clause not found in FROM clause
  • PDF rendering exercised with Polish, Turkish, Cyrillic and Baltic names plus a CJK substitution case
  • npx nest build verified to place both the templates and the fonts into dist
  • candidate counts measured against production: 355 accounts in scope for RiskProfile/FormA, 82 of
    them without any DfxApproval or FinancialData step

Three points in the PR description need an operator decision before the cutover.

@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

Five review passes were needed before two independent reviews came back with no findings.

What the passes changed, beyond the corrections already listed in the description:

  • Pass 1 — risk flags could be written while a sanctions review was still open; the same document
    could be generated from two different advisory-lock keys; the approval audit entry did not record
    the previous values of the four compliance columns the same transaction overwrites; the new cron
    job was missing from the job inventory; the unique index carried a hand-written name.
  • Pass 2 — CustomerProfile turned out to have the same two-lock-key gap that RiskProfile and
    FormA had. Every generated document now has exactly one generation path and one lock key.
  • Passes 3-4 — import order across the touched files.
  • Pass 5 — no findings from either review.

Two reported points were deliberately not applied, with the reasoning recorded in the pull request:
moving the approval gate onto the entity (it aggregates state from a separate query and the document
matrix of this workflow, which entity getters here deliberately do not carry), and making the
RiskProfile requirement conditional on the country (that would loosen a compliance gate and is an
operator decision, not a review correction - a test now pins the coupling instead).

The overlay text was drawn with a PDF standard font, which is limited to WinAnsi and throws on
every character outside Latin-1. Names, streets and employers in Polish, Turkish, Baltic, Czech or
Cyrillic spelling therefore failed the whole document, leaving the case without the evidence its
approval depends on - silently, once per minute.

The text is now set in an embedded Liberation Sans, which is metrically compatible with
Arial/Helvetica, so the layout is unchanged. Only the glyphs actually used are embedded (about 9 kB
per document). Characters the font has no glyph for, such as CJK, are substituted instead of
failing the document.
The reuse path fetched the whole blob only to find out whether it exists, and discarded it. It now
lists the key instead and fails loudly when a document that the database marks valid is gone from
storage.

It also exposes the registered file of a generation key, so a retry after a failed upload can keep
the name of the first attempt - that same string is printed into the document as its document
number, so name and content must not drift apart.
Two loads in the workflow could not work against a real database, and the mocked tests hid it:

- The completion loaded the step without its user data relation. `KycStep.userData` is a ManyToOne
  without eager loading, so it stayed undefined and reading its ID threw for every case. The step is
  now loaded through a query builder that joins the user data and locks the step alone.
- The risk-data initialisation locked user data with a plain find. That joins the eager, nullable
  relations of UserData, and Postgres rejects FOR UPDATE on the nullable side of an outer join. It
  now locks through a query builder without those joins.

Both lock aliases are lower case on purpose: the driver emits FOR UPDATE OF <alias> unquoted and
Postgres folds it to lower case, so a camelCase alias is not found in the FROM clause.

Beyond the two loads:

- RiskProfile and FormA follow the account, not a KYC step. Requiring a DfxApproval or FinancialData
  step as an anchor silently skipped every account without one, which the productive Sheet covers.
  Both documents are now generated from user data alone, serialised on a user-data advisory lock.
- Missing NameCheck evidence only fails the NameCheck document instead of the whole case.
- The gate blockers are logged instead of discarded: a case waiting forever was indistinguishable
  from a case nobody looked at.
- The excluded account IDs move from the source tree into the `dfxApprovalDocumentExclusions`
  setting, and the level log keeps the wording of the existing approval path.
Records the embedded Unicode font, the account-bound selection of RiskProfile and FormA, the
exclusion setting that has to be filled before the cutover, and the gate-blocker log line as a
monitoring source.
Every other document under docs/ is in English, and this repository is public. Same content, plus
the operator account is no longer named explicitly.
Three points from the review round:

- The batch that initialises empty risk fields passed `hasOpenNameChecks = false` instead of asking.
  A new sanctioned hit does not refresh `lastNameCheckDate` - that only happens on a clean result -
  so a still-valid date is no proof that no check is open. Such an account could get
  `highRisk = false` written, and a RiskProfile stating exactly that, while a sanctions match was
  still unresolved. The flag is now queried per account.
- RiskProfile and FormA were reachable from two different advisory-lock namespaces: the step lock in
  the approval loop and the user-data lock in the account-bound batch. Two instances could therefore
  render and upload the same document concurrently under the same generation key, each with its own
  embedded timestamp. Both documents now belong to the account-bound path alone.
- The approval audit entry recorded only step status, KYC level and KYC status, while the same
  transaction also overwrites `complexOrgStructure`, `highRisk`, `depositLimit` and `amlAccountType`.
  A limit set by Compliance was therefore not recoverable from the database. All four previous values
  are now part of the entry, which is written before the columns change.

The log entries and the step completion now go through the existing `KycLogService.createLogInternal`
and `KycStep.complete()` instead of rebuilding both by hand.
…ntory

- The unique index on `kyc_file.generationKey` carried a hand-written name, which the contribution
  guide rules out twice. Entity and migration now use the name TypeORM generates itself, and the
  constraint check in the storage service refers to the same constant.
- The new minute-interval job was missing from the cron inventory the contribution guide requires.
  Added, with the counts recomputed the way that document describes: 132 declarations across 93
  files, 111 of them with a flag, 50 on a minute interval.
CustomerProfile had the same cross-lock gap that RiskProfile and FormA had: the approval loop
requested it under the lock of the DfxApproval step while the batch generated it under the lock of
the FinancialData step. The two candidate sets overlap exactly at `kycLevel = 40`, so two instances
could render and upload the same document concurrently, each with its own embedded document number.

Each document now has a single path and a single lock key: GwGFileCover, IdentificationForm and
DfxNameCheck under the DfxApproval step lock, CustomerProfile under the FinancialData step lock,
RiskProfile and FormA under the user-data lock. That makes the combined selection helper redundant.

Also: the module imports are back in alphabetical order, and the comment on the exclusion setting
now says what the setting actually affects.
The contribution guide asks for alphabetically sorted imports. Moving the status DTO out of the
`output` directory and adding the fontkit import had left four import blocks out of order.
Applies the repository's import order - non-relative paths first, then relative ones, each
alphabetically - to all files this pull request touches, not just the ones spotted so far.
Three properties of the Sheet process are reproduced as they are - the compliance defaults written
on approval, the RiskProfile requirement outside a FATF-enabled country, and skipping documents that
already exist. Each is a property of the process being replaced, not of this implementation, and
changing any of them would change the outcome rather than the mechanism.

The residual risk is stated as well: the selection rules of the six document sheets come from the
audit package rather than from their source, because the script export of those projects is refused.
@DfxCron now requires scope; the workflow writes KYC records and
belongs on the worker, not the API process.
develop's download path still uses getBlob; the generated-file
helper uses listKeys. The suite needs both spies after the rebase.
@TaprootFreak
TaprootFreak force-pushed the codex/dfxapproval-api-workflow branch from a505cb1 to 9d3f340 Compare August 13, 2026 21:24
Prettier failed CI on docs/cron-jobs.md after the rebase edit.
@TaprootFreak
TaprootFreak marked this pull request as draft August 13, 2026 21:54
CONTRIBUTING requires every touched file at 100% on all four metrics
and a ratchet pin in the same PR. The new workflow, PDF, check,
document, catalog and module files now have specs that reach that
bar; the cutover flag and advisory-lock fakes are declared.
Map personal source_of_funds onto the seven CustomerProfile
categories, treat unknown PEP as not-no, generate RiskProfile
only when pep is false, reuse a valid generation-key winner,
reload account data under the user-data lock, and keep only
the newest FinancialData step per customer profile.
FinancialData stores multiple-choice answers as a comma-separated
list. Split before ticking the template categories, and assert the
risk-init query uses the 90-day lastNameCheckDate window.
The gate result is never exposed as an HTTP contract, so the
repo rule is interface rather than class.
Skip already-complete files so the 500-row window cannot starve
older cases, lock UserData inside the approval transaction, and
look up generation keys through KycFileService instead of a
forwarding wrapper.
Drop the pre-filter row cap so already-complete cases cannot hide
later work, keep the newest FinancialData step per account, and
mark a leftover WORM object valid instead of uploading it again.
Empty compliance values must not be drawn as no. Sort the KycFile
entity spec imports alphabetically.
A known true value still needs the document so Compliance can
see it. Only an unset flag stays out of generation.
The document key is per user. Two FinancialData sequences of the
same account must not write it under two step lock keys.
The generation key is per account. The FinancialData step is only
the selection source, not the lock key.
A hit that lands between the pre-check and the update must not
still receive the empty-field defaults.
Default the exists double to false so the write path stays
reachable, and pin both the stale-date and late-hit cases.
Extract the in-lock skip condition so both a stale name-check date
and a late sanctioned hit are asserted, including the empty update
after another worker already filled the columns.
Record the step comment before clearing it. Generate FormA and
RiskProfile only after verifiedName is set, and IdentificationForm
only after identificationType is set, so a later name or type
cannot auto-approve an empty first render.
Do not render or freeze a DfxApproval PDF that would silently omit
the verified name or default an unknown identification type to
OnlineId. The approval gate treats whitespace-only names as missing.
Match the existing KYC jobs instead of rolling date arithmetic.
The lock double and storage mock drifted, and getByGenerationKey
took the line that still named getUserDataKycFiles.
The workflow and document services add ten database reads that the
published inventory did not list. Rows and widths come from the
inventory scan of this head; advisory locks stay out of the table.
Permanently not-ready MANUAL_REVIEW steps no longer occupy the oldest-50
window: the review query orders by updated and a not-ready evaluation
bumps that timestamp. Advisory-lock unlock failures still release the
query runner. Document generation now selects candidate ids instead of
the full user graph, and getByGenerationKey no longer loads eager
relations.
A sanctioned name check, a thrown PDF/storage error, or a failed
CustomerProfile render no longer pins the same 50/500 ids at the front
of the next cron tick. Test-reality line numbers follow the spec.
The migration contract substitutes characters the font cannot encode
instead of aborting the document, and those substitutions must be
visible per case.
@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

EN:
Ready after 9 review passes.
This pull request replaces the live DfxApproval Google Sheets with a default-off API workflow that generates the six compliance documents and completes personal KYC when the gate is clear.

DE:
Bereit nach 9 Review-Durchläufen.
Dieser Pull Request ersetzt die produktiven DfxApproval-Google-Sheets durch einen default-off API-Workflow, der die sechs Compliance-Dokumente erzeugt und persönliches KYC abschliesst, sobald das Gate frei ist.

Details

Review: Grok quality + logic PASS_CLEAN, then Codex quality + logic PASS_CLEAN, both STATUS complete at 0b6f4652d. Earlier Codex findings that were accepted (load-sites inventory, HOL rotation, unlock/release nesting, ID-first generation, eager getByGenerationKey, test-reality line numbers, NameCheck load only when needed, log case ids, required document list, glyph-substitution log) are in subsequent commits. Rejected with repo evidence: admin CAS on kyc-admin.service.ts, highRisk-IS-NULL-only batch (sheet parity), Util.daysBefore DST, storage existence check of already-valid catalog rows on every cron tick, FATF as a separate gate blocker.

Comments: paginated issue comments (2 historical author notes, no open request), reviews 0, inline 0, unresolved review threads 0.

Mergeable: MERGEABLE. mergeStateStatus is BLOCKED only because the PR is still a draft.

CI at 0b6f4652d: all pull_request checks SUCCESS except Handbook Build Check SKIPPED (paths filter, no handbook change). Coverage ratchet SUCCESS. No required status checks on develop from classic protection or rulesets.

@TaprootFreak
TaprootFreak marked this pull request as ready for review August 14, 2026 05:42
@TaprootFreak
TaprootFreak merged commit 19a2887 into develop Aug 14, 2026
23 checks passed
@TaprootFreak
TaprootFreak deleted the codex/dfxapproval-api-workflow branch August 14, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant