toga-ai 1.0.956 → 1.0.958
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/features/per-client-database-connections.md +4 -1
- package/knowledge/2.0/apps/worker2/features/codeception-integration-tests.md +5 -2
- package/knowledge/2.0/apps/worker2/features/committed-secrets-and-config-debt.md +12 -5
- package/knowledge/2.0/apps/worker2/features/qa-qc-review-pipeline.md +13 -1
- package/knowledge/clients/nycdoe/features/asn-sftp-ingestion-2-0.md +37 -18
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +6 -4
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-06
|
|
10
10
|
owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Database.php
|
|
@@ -61,6 +61,8 @@ When `_underscore` serves a request for a client it opens **three distinct per-c
|
|
|
61
61
|
|
|
62
62
|
- **`Core.Clients` has rows for clients whose DBs were never built** (prod 2026-09-24: 46 rows, 35 built). "DB exists" = a `Core.DatabaseHosts` row (`databaseId` + `environmentId` [+ `regionId`]) for the current `Environments.slug` — the same rows `registerClientDatabases()` resolves. Any fan-out over all clients must filter `EXISTS` on `DatabaseHosts` for the env, not `Core.Clients` alone; `isActive`-style flags are wrong (inactive clients still have DBs). Host-row count varies (e.g. `Archive_Nychh` has 1, others 4) — still built. Example: [Tools Multi-Client SQL](../../../../1.0/apps/tools/features/multi-client-sql.md).
|
|
63
63
|
- **⚠ NEVER read `LAST_INSERT_ID()` (or any other per-connection session value) through `_Query`.** `_Query` defaults `_isReadHostEnabled = true` (`Query.php:8`) and routes SELECTs to `CONNECTION_READ` (`Query.php:273-276`). `LAST_INSERT_ID()` is scoped to the connection that did the INSERT, so a read-host SELECT returns **0** — silently wrong, and now an FK violation wherever the id is used as a foreign key. Use `mysqli_insert_id` on the write/transaction connection and reject `<= 0`. It is not biting today only because most client registrations leave `writeHost`/`readHost` null, and `Database.php:488` then points READ and WRITE at the **same** `mysqli` handle (so raw `mysqli_query` and `new _Query(...)` share one connection and one transaction). Configuring a real read host for any client would break every such call at once. Worked example: [Compass PEOPLE-file user lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
|
|
64
|
+
- **Locks and just-committed rows: force the writer and skip the cache.** A `_Query` SELECT goes to the read host and may be served from the query cache. For `GET_LOCK`/`RELEASE_LOCK` (session-scoped — must be on the writer) or reading a row another job just committed, call `$query->setisReadHostEnabled(false)` and `_Database::useQueryCache(false)`. Worked example: NYCDOE `Sftp/Import` per-checksum lock in [2.0 ASN SFTP Ingestion](../../../../clients/nycdoe/features/asn-sftp-ingestion-2-0.md).
|
|
65
|
+
- **`_Database::transactionCommit()` / `transactionRollback()` with NO database argument leave each alias's "started" flag `true`.** A later `transactionStart()` in the same PHP process then never begins a transaction — writes autocommit and a rollback undoes nothing. Prod worker jobs and api2 requests are safe (one job/request = fresh process state); it bites long-lived processes and test harnesses. Reset with `\_Database::$_transactionStarts = []` before `transactionStart()` (see `runAsWorkerJob` in [worker2 Codeception tests](../../worker2/features/codeception-integration-tests.md)).
|
|
64
66
|
- **Raw `mysqli_query` bypasses `_Query`'s automatic query-cache reset.** After a hand-rolled write, call `_Database::resetQueryCache($dbName)` yourself or later model reads serve stale rows.
|
|
65
67
|
|
|
66
68
|
- **NEVER build a client's database name as `'Client_' . $clientIdentifier`.** They diverge in production (`Compass_Usa` → `Client_Compass`). Resolve `Core.Clients.clientDatabaseId` → `Core.Databases.name`. This mirrors the log-database resolution already done in `_underscore`'s `Email.php`. A string-built name fails only for the clients whose names happen to diverge, so it passes local testing and breaks in production.
|
|
@@ -92,5 +94,6 @@ Methodology lesson — why the wrong conclusion was reached: the model-class com
|
|
|
92
94
|
- [Re-pointing a DB alias mid-request](./database-alias-repointing.md)
|
|
93
95
|
|
|
94
96
|
## Change history
|
|
97
|
+
- 2026-10-06 — Writer+no-cache reads for locks/just-committed rows; no-arg commit/rollback leaves the transaction-started flag set (mhammontree)
|
|
95
98
|
- 2026-09-23 — Added the read-host routing trap: `_Query` sends SELECTs to `CONNECTION_READ`, so `LAST_INSERT_ID()` read through `_Query` can return 0; noted that null `writeHost`/`readHost` currently collapses both to one handle, and that raw `mysqli_query` needs a manual `resetQueryCache()`. (mhammontree)
|
|
96
99
|
- 2026-09-24 — Added gotcha: filter client fan-outs by `DatabaseHosts` for the environment; `Core.Clients` includes never-built clients. (jcardinal)
|
|
@@ -6,13 +6,14 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-06
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/codeception.yml
|
|
13
13
|
- worker2/tests/Integration.suite.yml
|
|
14
14
|
- worker2/tests/Support/Helper/Integration.php
|
|
15
15
|
- worker2/tests/Integration/ParseCest.php
|
|
16
|
+
- worker2/tests/Integration/ImportDecisionCest.php
|
|
16
17
|
- worker2/tests/local.env.php.example
|
|
17
18
|
- worker2/composer.json
|
|
18
19
|
- worker2/.gitattributes
|
|
@@ -28,7 +29,7 @@ First 2.0 app-level automated test suite: Codeception 5 / PHPUnit 11 in worker2
|
|
|
28
29
|
|
|
29
30
|
## Summary
|
|
30
31
|
|
|
31
|
-
worker2 is the **pilot repo** for a 2.0 test framework. One suite, `Integration` (not `unit`/`functional` — the code under test needs a real DB), boots the real `_underscore` framework and calls worker actions in-process against a **local** client DB.
|
|
32
|
+
worker2 is the **pilot repo** for a 2.0 test framework. One suite, `Integration` (not `unit`/`functional` — the code under test needs a real DB), boots the real `_underscore` framework and calls worker actions in-process against a **local** client DB. Coverage: `Sftp/Parse` (`ParseCest`) and `Sftp/Import` `RunForFile` decisions (`ImportDecisionCest`) — 18 tests as of 2026-10-06. **Tests are a manual/local step — never a deploy-time gate.** 1.0's pattern (`codecept.phar` + `.ebextensions/040_run_tests.config` running tests against production on every deploy) was deliberately NOT copied.
|
|
32
33
|
|
|
33
34
|
## How it works
|
|
34
35
|
|
|
@@ -43,9 +44,11 @@ worker2 is the **pilot repo** for a 2.0 test framework. One suite, `Integration`
|
|
|
43
44
|
## Gotchas
|
|
44
45
|
|
|
45
46
|
- **Calling a worker action directly bypasses `_Route`, so there is no open transaction** — without the explicit `_Database::transactionStart()` the commit is a silent no-op and reads miss writes. Do not work around the symptom (e.g. disabling read-host connections); start the transaction. See [CLI script bootstrap](../../_underscore/features/cli-script-bootstrap.md).
|
|
47
|
+
- **Testing code that commits, rolls back, then writes again (worker job semantics): use `runAsWorkerJob`** (`tests/Support/Helper/Integration.php`). `_Database::transactionCommit()` / `transactionRollback()` with no argument leave the per-alias "started" flag set, so a second `transactionStart()` in the same process does nothing (writes autocommit, rollback undoes nothing). The helper resets `\_Database::$_transactionStarts = []` before `transactionStart()` to mimic a fresh worker request. Framework detail: [Per-Client Database Connections](../../_underscore/features/per-client-database-connections.md).
|
|
46
48
|
- **Do not `chdir` back after booting.** `Loader::autoloader` resolves project classes against the CWD lazily at first use, so restoring the old CWD breaks autoloading of `_Worker_*` classes mid-test.
|
|
47
49
|
- **`_Database::$_registers` is public static** — read it directly; no Reflection.
|
|
48
50
|
- **Prove a regression test fails without the fix** before trusting it (done for the Lenovo comma repair: serial mismatch once columns shift).
|
|
49
51
|
|
|
50
52
|
## Change history
|
|
53
|
+
- 2026-10-06 — ImportDecisionCest added; runAsWorkerJob helper for the stale transaction-flag trap (mhammontree)
|
|
51
54
|
- 2026-09-25 — Created: Codeception/PHPUnit pilot, Integration suite for Sftp/Parse; cto-reviewed design, php-reviewer fixes to the harness (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
10
|
-
owners: ["kyalamarthi", "ajean"]
|
|
9
|
+
updated: 2026-10-06
|
|
10
|
+
owners: ["kyalamarthi", "ajean", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/.gitignore
|
|
13
13
|
- worker2/Worker/Team/Github.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- worker2/Config/production.ini
|
|
16
16
|
- worker2/Config/netsuite_ec_key.pem
|
|
17
17
|
- worker2/Worker/Team/Sprint.php
|
|
18
|
+
- worker2/.platform/php/conf.d/00-log-errors.ini
|
|
18
19
|
related:
|
|
19
20
|
- ../workflows/clickup-credential-rotation.md
|
|
20
21
|
- ./automated-pr-merger-force-push-race.md
|
|
@@ -47,6 +48,13 @@ the publish on a credential literal.
|
|
|
47
48
|
| TOGa Talos API key — **current** (`[talos] api_key`) | `worker2/Config/production.ini` `[talos]` group (~L230) | Shared by release notes and the transcript pipeline. Committed like every `Config/*.ini` (see Summary). |
|
|
48
49
|
| PayPal webhook signature verification **off** in production | `worker2/Config/production.ini:182` | Impact already written up — a caller who knows the URL can create a paid SalesOrder. See [Rate PayPal subscription webhook](../../../../clients/rate/features/paypal-subscription-purchase-webhook.md). |
|
|
49
50
|
|
|
51
|
+
### Secrets stored at runtime in `Core.WorkerJobs` (not committed, same exposure)
|
|
52
|
+
|
|
53
|
+
| Exposure | Location | Status |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Stack traces include function arguments (first 15 chars of each string) | worker2 stores failed-job traces in `Core.WorkerJobs`; `.platform/php/conf.d/00-log-errors.ini` does **not** set `zend.exception_ignore_args = On` | **DevOps option, not done (Mark: devops decision).** Turning it on strips arguments from every trace. Known leak: phpseclib `login()` frames carry the password — NYCDOE `Sftp/Import` `connect()` works around it by not chaining the exception. |
|
|
56
|
+
| NYCDOE vendor FTP passwords | `Core.WorkerJobs.parameters` of old 1.0 `Utilities/Sftp/DeleteFile/DeleteSftpFile` jobs (1.0 sent the password in the job; source = `FTP_*` constants in `library/app/asnprocessor/*.php`) | **Open — separate ticket: rotate.** See [NYCDOE ServiceNow / ASN Integration](../../../../clients/nycdoe/features/servicenow-integration.md). |
|
|
57
|
+
|
|
50
58
|
### Fixed / no longer true
|
|
51
59
|
|
|
52
60
|
- **ClickUp API token in `Sprint.php`** — the hardcoded `CLICKUP_API_ACCESS_TOKEN` constant is
|
|
@@ -72,9 +80,6 @@ the publish on a credential literal.
|
|
|
72
80
|
- **Keep this table current.** Add an entry whenever a committed credential is found, and move it to
|
|
73
81
|
*Fixed* when it is rotated and relocated, so this stays the one place the team looks.
|
|
74
82
|
|
|
75
|
-
## Change history
|
|
76
|
-
- 2026-09-25 — renamed product reference from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
|
|
77
|
-
|
|
78
83
|
## Related
|
|
79
84
|
|
|
80
85
|
- [Rotating the ClickUp API credential](../workflows/clickup-credential-rotation.md) — the worked rotation procedure; follow its shape for any credential here
|
|
@@ -84,5 +89,7 @@ the publish on a credential literal.
|
|
|
84
89
|
- [Rate PayPal Subscription Purchase & Webhook Pipeline](../../../../clients/rate/features/paypal-subscription-purchase-webhook.md)
|
|
85
90
|
|
|
86
91
|
## Change history
|
|
92
|
+
- 2026-10-06 — WorkerJobs runtime exposures: trace arguments (`zend.exception_ignore_args` devops option), NYCDOE FTP passwords in old job params (mhammontree)
|
|
87
93
|
- 2026-10-01 — TOGa Talos release-notes key moved to `[talos] api_key`; old key listed for revocation (ajean)
|
|
94
|
+
- 2026-09-25 — renamed product reference from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
|
|
88
95
|
- 2026-09-21 — Register created from audit (kyalamarthi)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-06
|
|
10
10
|
owners: ["jcardinal", "ajean"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Clickup/QaReview.php
|
|
@@ -110,6 +110,9 @@ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
|
|
|
110
110
|
**Needs Review** ([general automation](./clickup-general-automation.md) rule 7); `QaReview` /
|
|
111
111
|
`FeatureReview` ignore Needs Review, so there is no loop.
|
|
112
112
|
- **Deny** sends the task to **Rework** and **reverts** the task's changes from the QA/QC branch.
|
|
113
|
+
- **BE `Conflict`** (BE Review option orderindex 4) = Denied: Rework + clear Environment + revert
|
|
114
|
+
(`REVIEW_CONFLICT`, reason `be-review-conflict`). Entry reset already clears it; `replayEarlyReview`
|
|
115
|
+
does not replay it.
|
|
113
116
|
- **Unit-test gate is a STUB today.** `runUnitTests()` currently returns `true`. Real gating (Cypress /
|
|
114
117
|
Postman / Codeception) is pending — do not treat the gate as enforcing anything yet.
|
|
115
118
|
|
|
@@ -172,9 +175,17 @@ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
|
|
|
172
175
|
so `FeatureReview` owns them; `StageBatch` ships a Feature as one feature branch and finalizes its
|
|
173
176
|
subtasks (and skips subtasks in the batch scan).
|
|
174
177
|
- The feature's own **BE Review is a MANUAL field for now** (jcardinal + CTO decision).
|
|
178
|
+
- `rollUpBe`: feature's own BE **Conflict** → `reworkFeature` (`feature-be-conflict`); any subtask BE
|
|
179
|
+
**Denied or Conflict** → those subtasks + feature to Rework.
|
|
175
180
|
|
|
176
181
|
## Gotchas
|
|
177
182
|
|
|
183
|
+
- **A task already sitting in BE `Conflict` before the 2026-10-06 fix must be moved to Rework by hand** —
|
|
184
|
+
code reacts only to a field change.
|
|
185
|
+
- **ClickUP for Dev MCP is allowlist-scoped.** Its read scope must include Dev Sprints `90116585845`;
|
|
186
|
+
otherwise `get_task` on a custom id fails with "outside the authorized scope" or a misleading
|
|
187
|
+
"Invalid ClickUp API token". Workaround: prod `Team.Tasks` `customClickupIdentifier` →
|
|
188
|
+
`clickupIdentifier`, then `get_task` with the internal id.
|
|
178
189
|
- **Entry is on ENTERING the status, not leaving it.** The legacy trigger fired on leaving; the QA/QC
|
|
179
190
|
entry point had to move to the entering event or the whole chain never starts.
|
|
180
191
|
- **The unit-test gate is a stub (`runUnitTests()` returns true).** It does not block anything yet.
|
|
@@ -220,5 +231,6 @@ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
|
|
|
220
231
|
- [ClickUp custom-field conventions](./clickup-custom-field-conventions.md)
|
|
221
232
|
|
|
222
233
|
## Change history
|
|
234
|
+
- 2026-10-06 — BE Review `Conflict` now routes to Rework + revert like Denied (QaReview + FeatureReview) (jcardinal)
|
|
223
235
|
- 2026-10-01 — DevFinished CatchUp (cron 86); QC Batch turned on (id 75); review fields reset to Needs Review on entry; Stage deploys confirmed manual (ajean; worker2 PR #178, dbchanges2 PR #504)
|
|
224
236
|
- 2026-09-25 — Scope narrowed to Dev Sprints `90116585845` (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-06
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Sftp/Import.php
|
|
@@ -17,6 +17,8 @@ files:
|
|
|
17
17
|
- dbchanges2/Client_Nycdoe/2026-09-22b - Add ASN parsing fields to ServiceRequests.sql
|
|
18
18
|
- dbchanges2/Client_Nycdoe/2026-09-22c - Add customerPurchaseOrder field to Cases.sql
|
|
19
19
|
- worker2/tests/Integration/ParseCest.php
|
|
20
|
+
- worker2/tests/Integration/ImportDecisionCest.php
|
|
21
|
+
- dbchanges2/Client_Nycdoe/2026-09-14a - Add SFTP import status fields to Files.sql
|
|
20
22
|
- dbchanges2/Core/2026-09-14a - Add SFTP Import cron - NYCDOE Lenovo.sql
|
|
21
23
|
related:
|
|
22
24
|
- servicenow-integration.md
|
|
@@ -29,7 +31,7 @@ related:
|
|
|
29
31
|
|
|
30
32
|
## Summary
|
|
31
33
|
|
|
32
|
-
The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [ServiceNow / ASN Integration](servicenow-integration.md) for the 1.0 pipeline, still live). **ASN-first:** nothing is created in 2.0 except from an ingested ASN file. Phase 1 (fetch) and Phase 2 (parse) are built and verified locally against real sample files for all 4 manufacturers; Phase 3 (NetSuite SO, freeze/growth lineage) is not built. Not
|
|
34
|
+
The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [ServiceNow / ASN Integration](servicenow-integration.md) for the 1.0 pipeline, still live). **ASN-first:** nothing is created in 2.0 except from an ingested ASN file. Phase 1 (fetch) and Phase 2 (parse) are built and verified locally against real sample files for all 4 manufacturers; Gated delete (1.0 hands each file to 2.0) is built, not deployed. Phase 3 (NetSuite SO, freeze/growth lineage) is not built. Not scheduled in prod.
|
|
33
35
|
|
|
34
36
|
## How it works
|
|
35
37
|
|
|
@@ -47,23 +49,35 @@ The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [S
|
|
|
47
49
|
| Acer | CDW 47-col, 3 names differ from Lexmark | `ACER_HEADER` | `YYYY/MM/DD` |
|
|
48
50
|
| Apple | own | `APPLE_HEADER` | `order_date` `M/D/YY` or `M/D/YYYY` |
|
|
49
51
|
|
|
50
|
-
###
|
|
51
|
-
|
|
52
|
-
- **DECIDED (Mark, 2026-09-25/29): 1.0 must not delete a file until 2.0 has imported and parsed it.** Replaces the 2026-08-28 "copy to temp subfolder" bridge idea.
|
|
53
|
-
- **The 4 `dbchanges2/Core/2026-09-14a - Add SFTP Import cron - NYCDOE <Mfr>` rows ship `isActive=0`** (ordered rollout); they stay off through shadow mode.
|
|
54
|
-
- **PROPOSED design (2026-09-29, for review with Jeff; needs a fresh `cto` pass — the 2026-09-25 CTO agreement was for an earlier polling variant):**
|
|
55
|
-
1. 1.0 replaces its `DeleteSftpFile` runTask with `Sftp/Import/RunForFile(clientId, username, filePath)` — no password in SQS.
|
|
56
|
-
2. `RunForFile` loads **all** `Sftp/Import/Run` CronJobs rows for clientId+username (the CDW username returns both Lexmark and Acer rows — never trust a manufacturer from 1.0), matches `fnmatch` patterns (no match → delete directly, old behavior), fetches, saves `Files`, queues Parse with `deleteSourceOnSuccess=true`.
|
|
57
|
-
3. Parse on success queues new `Sftp/Import/DeleteSource(clientId, filesUuid)`, which re-checks `c_status` (`succeeded`/`released`) **and** the remote file's sha256 vs `Files.checksum` before deleting.
|
|
58
|
-
4. Checksum already in `Files`: `succeeded` → delete; stale `pending` → re-queue Parse; `failed` → keep the file.
|
|
59
|
-
5. 1.0 skips files already in `Logs.FileLog` (job+fileName+fileTimestamp) but still re-queues `RunForFile`.
|
|
60
|
-
6. Kill switch `[nycdoe] asn_delete_mode` = `gated` | `immediate`; missing = `immediate`.
|
|
61
|
-
7. Cutover: turn the CronJobs rows on with `deleteSourceOnSuccess`, remove the 1.0 processors.
|
|
62
|
-
8. Add `released` to `Files.c_status` for manual release of failed files.
|
|
52
|
+
### Gated delete (BUILT 2026-10-06, branch `TRUE-80519`, NOT deployed)
|
|
53
|
+
**Rule (Mark 2026-09-25, Jeff approved flow 2026-10-06): 1.0 never deletes a DOE ASN file; 2.0 deletes it only after it is imported and parsed.** Cutover = flip cron jobs 1.0 → 2.0; Phase 3 (NetSuite) later. 1.0 side: Stage 1b gated mode in [ServiceNow / ASN Integration](servicenow-integration.md).
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
1. **1.0 queues `Sftp/Import/RunForFile(clientId, username, filePath)`** in place of its delete — no password in SQS.
|
|
56
|
+
2. **`RunForFile`** loads **all** `Sftp/Import/Run` CronJobs rows for clientId+username (used only as config; rows stay `isActive=0` in shadow mode; the CDW account returns Lexmark + Acer rows — manufacturer is never taken from 1.0).
|
|
57
|
+
- Path must be exactly `remoteDirectory` + a plain file name (`isAllowedFilePath`), else refused.
|
|
58
|
+
- Name matches CronJobs param `retiredFileNamePatterns` (Lenovo row: `DOE-OPEN-LENOVO-*`) → deleted on sight.
|
|
59
|
+
- Name matches no pattern → **KEPT** on the SFTP (Mark: may be a real file sent under a wrong name).
|
|
60
|
+
- mtime < 120 s → wait (next trigger).
|
|
61
|
+
- Per-checksum MySQL `GET_LOCK` on the client **writer**, query cache off. Checksum lookup reads the writer and filters `c_status IS NOT NULL` (`Files` also holds non-SFTP files).
|
|
62
|
+
- Checksum already in `Files`: `succeeded`/`released` → queue `DeleteSource` with the triggering `filePath`; `pending` > 30 min → re-queue Parse; `processing` > 30 min → mark `failed` + throw; `failed` → keep.
|
|
63
|
+
- New file → fetch, save `Files`, queue Parse with `deleteSourceOnSuccess=true`.
|
|
64
|
+
3. **Parse** claims its `Files` row (`pending`→`processing`, committed; a second job returns `already_claimed`), commits, then queues `DeleteSource`. On error: roll back, then save status **outside** the job transaction — `pending` only for transient DB errnos (1213, 1205, 2006, 2013), else `failed`.
|
|
65
|
+
4. **`Sftp/Import/DeleteSource(clientId, filesUuid, filePath=null)`** deletes only if `c_status` is `succeeded`/`released` **and** the remote sha256 still equals `Files.checksum`.
|
|
66
|
+
5. **`Run()`** gained `deleteSourceOnSuccess` and `retiredFileNamePatterns` params and skips files modified < 5 min ago. Validates `fileModelClass` (`is_a _Model_Client_File`) and `adapter` (a method on `_Worker_Sftp_Parse`).
|
|
67
|
+
6. **Kill switch** is 1.0's `[nycdoe] asn_delete_mode` (missing = old immediate delete).
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
**Manual release of a failed file:** set `Files.c_status='released'`, then queue `Sftp/Import/DeleteSource` for that `filesUuid`.
|
|
70
|
+
|
|
71
|
+
**Before cutover (open):**
|
|
72
|
+
- `Run()` has no retry for a lost Parse or lost `DeleteSource` job (only `RunForFile` re-drives). A row stuck `processing` turns `failed` only on the next 1.0 trigger after 30 min.
|
|
73
|
+
- **Manual ASN files** are uploaded to the Lenovo folder (e.g. `WR260197522-RPL Manual ASN.csv`, 2026-09-14). 2.0 KEEPS them (no pattern) and imports nothing — decide how 2.0 handles manual files.
|
|
74
|
+
- Separate tickets (not done): SSH host-key pinning in `Sftp/Import` `connect()`; rotate the FTP passwords old `DeleteSftpFile` jobs likely stored in `Core.WorkerJobs.parameters` (see [worker2 secrets register](../../../2.0/apps/worker2/features/committed-secrets-and-config-debt.md)).
|
|
75
|
+
|
|
76
|
+
Schema (`dbchanges2/Client_Nycdoe/`): `2026-09-14a` Files `c_status` (ENUM `pending`/`processing`/`succeeded`/`failed`/`released`) + non-unique index on `Files.checksum`/`c_manufacturer`/`c_dtProcessed`; `2026-09-22a` ServiceRequestStages seed (New/Scheduled/On Hold/Delivered/Cancelled); `2026-09-22b` ServiceRequests fields; `2026-09-22c` Cases `c_customerPurchaseOrder`; `2026-09-25a` Cases `longDescription`.
|
|
77
|
+
|
|
78
|
+
Core `2026-09-14a` Lenovo row carries `retiredFileNamePatterns`. Neither `2026-09-14a` file had run in prod (verified 2026-10-06), so both were edited in place.
|
|
79
|
+
|
|
80
|
+
Tests: `ParseCest.php` (all 4 mfrs, header rejection, comma repair, claim, kept `failed` status) + `ImportDecisionCest.php` (RunForFile decisions, path check, retired names) — see [worker2 Codeception integration tests](../../../2.0/apps/worker2/features/codeception-integration-tests.md).
|
|
67
81
|
|
|
68
82
|
## Gotchas
|
|
69
83
|
|
|
@@ -71,9 +85,14 @@ Tests: `worker2/tests/Integration/ParseCest.php` (all 4 mfrs + header rejection
|
|
|
71
85
|
- **1.0 `asnprocessor/lenovo.php` is not a source of truth for today's Lenovo layout.** Its 47-col mapping is stale; the real file is 30 cols (confirmed on 3 live samples). Verify every header and date format against a **real current sample file**, never the 1.0 code — wrong Lenovo header, Lexmark date, and Apple 2-digit year (`'25'` → year `0025`) were all caught this way.
|
|
72
86
|
- **Acer and Lexmark share column positions, not header names** (Acer `First_Name_Shipping`/`Last_Name_Shipping`/`SerialNo` vs Lexmark `Contact_Name_Shipping`/`Email_Shipping`/`Svc_Tag_ID`). Keep separate header constants. Lenovo stays its own path (not a CDW supplier).
|
|
73
87
|
- **Lenovo unescaped comma in `Product Long Description` (col 18) is repaired, not rejected** — `repairLenovoRowColumnCount()` merges the extra fragments back (ported from 1.0; the only field Lenovo breaks this way). A true header mismatch is still fatal. No real occurrence seen in 2.0 yet — re-confirm when Lenovo next sends one.
|
|
74
|
-
- **`DOE-OPEN-LENOVO-*` files are a retired process — never parse; delete on sight** (Skyler/Eric). Lenovo's `
|
|
88
|
+
- **`DOE-OPEN-LENOVO-*` files are a retired process — never parse; delete on sight** (Skyler/Eric). Enforced by the Lenovo row's `retiredFileNamePatterns`.
|
|
89
|
+
- **Every CronJobs `params` JSON key must be a `Run()` parameter** — params are spread as named args, so an unknown key is a fatal error. Add the parameter before the key.
|
|
90
|
+
- **worker2 runs each job in one transaction** (`Route.php` `transactionStart`; `Controller/Index.php` rolls back on throw). A status save followed by `throw` is rolled back — the Phase 2 bug left rejected files `pending` forever. Save failure status after the rollback, outside the job transaction.
|
|
91
|
+
- **`connect()` must not chain the phpseclib exception** — phpseclib `login()` frames carry the first 15 chars of the password, and worker2 stores job traces in `Core.WorkerJobs`. It rethrows a fresh exception with no `previous`.
|
|
92
|
+
- **Locks and just-committed rows must read the writer** — see the read-host gotcha in [Per-Client Database Connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md).
|
|
75
93
|
- **Schema drift: `_Model_Client_Case` declares `longDescription` but `Client_Nycdoe.Cases` lacked the column** → any direct-model Case load/save 1054s. Fixed for DOE (`2026-09-25a`). Other tenants' `Cases` may have the same gap — unchecked (ask Rohan).
|
|
76
94
|
|
|
77
95
|
## Change history
|
|
96
|
+
- 2026-10-06 — Gated delete built (RunForFile/DeleteSource, Parse claim + status-outside-transaction fix, retired names, `released` status); open cutover items (mhammontree)
|
|
78
97
|
- 2026-09-29 — Activation plan: gated-delete decision, proposed RunForFile/DeleteSource design, CronJobs rows set isActive=0 (mhammontree)
|
|
79
98
|
- 2026-09-25 — Phase 2 parsing built for all 4 mfrs; header validation, comma repair, real-sample header/date corrections; integration tests added (mhammontree)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-06
|
|
10
10
|
owners: [mhammontree, sking]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/import_asn.php
|
|
@@ -28,6 +28,7 @@ files:
|
|
|
28
28
|
- worker/schedules/cron.worker.notification.json
|
|
29
29
|
- library/app/api/nycdoe.php
|
|
30
30
|
- library/app/nycdoe.php
|
|
31
|
+
- library/app/asnprocessor.php
|
|
31
32
|
- library/app/asnprocessor/manufacturer.php
|
|
32
33
|
- library/app/asnprocessor/apple.php
|
|
33
34
|
- library/app/asnprocessor/lenovo.php
|
|
@@ -90,7 +91,7 @@ The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents +
|
|
|
90
91
|
| EDI invoices out (NetSuite saved search 2108) | `send_edi_open_invoices.php` | every 2 h at :13 |
|
|
91
92
|
| Installation schedule reports | `notifications/nycdoe/send_installation_schedules_doe.php` (3 business days ahead, weekends skipped, **holidays NOT skipped**), daily + current-week variants, weekly part-orders | 5–8 AM daily/Mondays |
|
|
92
93
|
|
|
93
|
-
**Unscheduled / dormant:** `download_tickets*.php` (superseded by `import_inc.php` / `import_asn.php`), `DOA_*` variants, `doe_install_fix.php`, `send_installation_schedules.php` (base variant). Note `sync/syncro/download_tickets.php` in the schedule is the **Syncro** integration, not NYCDOE. A second `crons/infrastructure/import_asn.php` also
|
|
94
|
+
**Unscheduled / dormant:** `download_tickets*.php` (superseded by `import_inc.php` / `import_asn.php`), `DOA_*` variants, `doe_install_fix.php`, `send_installation_schedules.php` (base variant). Note `sync/syncro/download_tickets.php` in the schedule is the **Syncro** integration, not NYCDOE. A second `crons/infrastructure/import_asn.php` also calls the asnprocessors but is **not scheduled** — only `legacy_import_asn.php` runs.
|
|
94
95
|
|
|
95
96
|
### The ASN pipeline
|
|
96
97
|
```
|
|
@@ -111,8 +112,8 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
111
112
|
|
|
112
113
|
- **Stage 1a (API):** vendor detected by string match on `u_asset_mfg` (apple=1, acer=2, lenovo=3, lexmark=4; unknown → Lexmark). Serial 'S'-prefix stripped for Lexmark/Lenovo. Ship-to resolved via `App_Api_NYCDOEV2::resolveShippingAddress()`; on failure a tracking `NYCDOETicket` is recorded but **ASN creation is skipped**. `dedupeKey = vendorId|PO|DOE-part|serialNumber`. Dedup is SELECT-before-INSERT with per-row try/catch (unique index is the race backstop).
|
|
113
114
|
- **Stage 1b (SFTP):** the only source of **non-serialized** items (in practice the Lexmark feed). Serial-less lines of the same PO+part are summed per file into one queue row with `dedupeKey = vendorId|PO|DOE-part|<file token>`. Dedup is INSERT + catch "Duplicate entry" (race-safe).
|
|
114
|
-
- **Per file, in order** (`App_ASNProcessor_{Lenovo,Apple,Lexmark}::processFiles()`): read over plain FTP (`App_FTP`: `ftp_connect`/`ftp_rawlist`/`ftp_get`) → save legacy `Logs.FileLog` (jobs `ASN_LENOVO` / `ASN_APPLE` / `ASN_LEXMARK
|
|
115
|
-
-
|
|
115
|
+
- **Per file, in order** (`App_ASNProcessor_{Lenovo,Apple,Lexmark}::processFiles()`): read over plain FTP (`App_FTP`: `ftp_connect`/`ftp_rawlist`/`ftp_get`) → parse → cache to disk → email → save legacy `Logs.FileLog` (jobs `ASN_LENOVO` / `ASN_APPLE` / `ASN_LEXMARK`; the row means "done") → `App_ASNProcessor::releaseSourceFile()`. A file is never released before it is logged.
|
|
116
|
+
- **`asn_delete_mode` (`[nycdoe]` in `worker/config.worker.ini`; built 2026-10-06 on `TRUE-80519`, NOT deployed):** missing/`immediate` = original behavior — queue `App_Worker::runTask('Utilities/Sftp/DeleteFile/DeleteSftpFile')`, worker2 `Worker/Utilities/Sftp/DeleteFile.php` deletes over SFTP (phpseclib3), delete gated only on the disk-cache write, not a good parse. ⚠ That path sends the SFTP password **inside the SQS message** (source: `FTP_*` constants in `asnprocessor/*.php`) — see the credentials gotcha. `gated` = queue 2.0 `Sftp/Import/RunForFile` (clientId/username/filePath, no password); 2.0 deletes only after its own import+parse — see [2.0 ASN SFTP Ingestion](asn-sftp-ingestion-2-0.md) (Gated delete). Gated mode also skips files already in `Logs.FileLog` (`isFileAlreadyLogged()`: job + fileName + fileTimestamp via `App_Date::convertToSQLDate`, same as `App_Model`) but still re-releases them. **Activate:** set the ini value + worker deploy. Default config verified to behave exactly as before (offline under PHP 7.2; SQL against legacy prod).
|
|
116
117
|
- **No Acer processor runs.** `asnprocessor/acer.php` is dead code (not called from `legacy_import_asn.php`, never appends to `$output`, never deletes). Acer files do arrive — via CDW on the shared CDW/Lexmark SFTP account — and Lexmark `processFiles()` has **no file-name filter**, so 1.0 processes every Acer file **as Lexmark** (Lexmark vendorId). Proof: 458 `ASN_LEXMARK` FileLog rows with `acer` in `fileName` (2025-08-18 → 2026-09-25), 0 `ASN_ACER`. Any 1.0-vs-2.0 count must compare 1.0 Lexmark against 2.0 Lexmark + Acer.
|
|
117
118
|
- **Vendors upload and forget** — they never re-send or notice if a file sits on the SFTP (Mark, 2026-09-25). A deleted-but-unparsed file is gone unless recovered from `Logs.FileLog`.
|
|
118
119
|
- **Stage 2:** queue → ASN/Items/Units/TrackingNumbers in a transaction. Serialized: one Unit per serial. Non-serialized: the file's quantity is a **cumulative** total — `unsent item qtyOrder = cumulative − SUM(committed qtyOrder)`; post-freeze growth becomes a **fresh item row**, a decrease fires a discrepancy email and changes nothing. Exactly one Unit per serial-less item row regardless of quantity. Failed rows retry 5× then alert `devteam@goagilant.com`.
|
|
@@ -268,6 +269,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
|
|
|
268
269
|
- Local deep-dive (full WR260236464 case study with record IDs): `doe_service_now_integration.md` in Mark's WWW workspace root; also the `service-now-integration` skill (`asn-import-flow.md`)
|
|
269
270
|
|
|
270
271
|
## Change history
|
|
272
|
+
- 2026-10-06 — Stage 1b gated delete mode (`asn_delete_mode`, `releaseSourceFile`, FileLog saved before release) built, not deployed (mhammontree)
|
|
271
273
|
- 2026-09-29 — Stage 1b file-delete path (delete not gated on parse; SFTP password rides in SQS), Acer-as-Lexmark, FileLog index facts (mhammontree)
|
|
272
274
|
- 2026-09-29 — Assignment-group scoping corrected (inverted `isValidDepartmentName`, three competing group lists) with detail in hold-status-sync; `Logs.API` 30-day retention shipped. (mhammontree)
|
|
273
275
|
- 2026-09-25 — TRUE-82332: cross-link to the close-clobber fix; test-repo copy of the ServiceNow credential noted (mhammontree)
|
package/package.json
CHANGED