019fb836 DfxApproval-GSheets durch API-Workflow ersetzen - #4553
019fb836 DfxApproval-GSheets durch API-Workflow ersetzen#4553TaprootFreak wants to merge 15 commits into
Conversation
18f69f6 to
d05a9d7
Compare
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.
|
Verification state after the review corrections:
Three points in the PR description need an operator decision before the cutover. |
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.
|
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:
Two reported points were deliberately not applied, with the reasoning recorded in the pull request: |
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.
Purpose
Replaces the eight minute-interval personal DfxApproval GSheets with a fail-closed API workflow.
Context: DFXswiss/gsheet#9.
Sheets replaced
IdentReportandPersonalNameCheckstay in their existing KYC processes and remain part of theapproval gate. Organizations are not part of this personal migration.
Implementation
DfxApproval,FinancialDataandpersonal accounts on level 30-40)
embedded Unicode font
generationKeyand DfxNameCheck under the DfxApproval step lock, CustomerProfile under the FinancialData step
lock, RiskProfile and FormA under the user-data lock
DFXswiss/servicesCorrections after review
Two loads could not work against a real database, and the mocked tests hid both. Every correction
below was reproduced and then re-verified against a real PostgreSQL 16 with the driver version this
repository pins.
KycStep.userDatais aManyToOnewithout eager loading, so it stayedundefinedand reading itsID threw for every case - caught by the cron and only logged. The step is now loaded through a
query builder that joins the user data and locks the step alone.
nullable relations of
UserData, and PostgreSQL rejectsFOR UPDATEon the nullable side of anouter 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, so a camelCase alias is notfound in the FROM clause.
limited to WinAnsi and throws on Polish, Turkish, Baltic, Czech or Cyrillic characters - the case
then waits forever without its evidence. The text is now set in an embedded Liberation Sans
(metrically compatible with Arial/Helvetica, about 9 kB embedded per document); characters without
a glyph are substituted instead of failing the document.
the workflow required a
DfxApprovalorFinancialDatastep as an anchor. Measured againstproduction, 82 of 355 current candidates have neither. They are now generated from user data alone.
NameCheck evidence also lost its GwGFileCover and IdentificationForm. It now fails only the
NameCheck document.
risk fields assumed no name check was open. A new sanctioned hit does not refresh
lastNameCheckDate- that only happens on a clean result - so the date was no proof. The flag isnow queried per account.
were reachable from two entry points with different advisory-lock keys, 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.
while the same transaction also overwrites
complexOrgStructure,highRisk,depositLimitandamlAccountType. A limit set by Compliance was therefore not recoverable from the database. Allfour previous values are now part of the entry, written before the columns change.
Smaller corrections: gate blockers are logged instead of discarded, so a waiting case is
distinguishable from one nobody looked at; the excluded account IDs moved from the source tree into
the
dfxApprovalDocumentExclusionssetting; the storage reuse path lists the key instead ofdownloading the whole document and fails loudly when a document marked valid is gone; a regenerated
document keeps the file name of the first attempt, which is also printed into it as its document
number; values that do not fit their field are logged instead of being cut silently; the level log
keeps the wording of the existing approval path; the new cron job is in
docs/cron-jobs.md; theindex name, import order and log helpers follow the repository conventions.
Cutover
The cron stays off after the deployment. Fill
dfxApprovalDocumentExclusionswith the productivelegacy exceptions, disable and verify all eight Apps Script triggers, then set
KYC_DFX_APPROVAL_WORKFLOW_ENABLED=true. Rollback and operational checks are indocs/dfx-approval-migration.md.Verification
TypeErrorandFOR UPDATE cannot be applied to the nullable side of an outer joinnpm run lint,npx nest build, and the builtdistverified to contain the templates and fontsOpen questions for the operator
depositLimitis set to 100000 unconditionally, overwriting a different existingvalue. Is that the intended Sheet behaviour, or should an existing limit be preserved? The
previous value is now recorded in
kyc_logeither way.FATF-enabled country of residence - the same combination the Sheet process had. An account outside
such a country therefore stays with Compliance instead of being approved automatically. No country
in the seed data has
fatfEnable = false; a test pins the coupling so both sides can only bechanged together. Leave as is, or make the requirement conditional?
the SQL header holding the
wherecell is not part of it, and six of the eight scripts could notbe exported. The candidate sets should be compared against one live sheet before the cutover.
regenerated after this change.