toga-ai 1.0.669 → 1.0.671
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/item-fulfillment-stage-lifecycle-and-order-status.md +26 -9
- package/knowledge/2.0/apps/saml/INDEX.md +1 -0
- package/knowledge/2.0/apps/saml/workflows/rotating-the-saml-sp-certificate.md +109 -0
- package/knowledge/2.0/standards/ssl-certificate-trust-model.md +82 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/features/order-fulfillment-status-per-line.md +153 -55
- package/knowledge/clients/compass-canada/profile.md +15 -9
- package/knowledge/clients/compass-usa/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/features/asn-to-item-fulfillment.md +82 -1
- package/knowledge/clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md +102 -3
- package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +95 -5
- package/knowledge/clients/compass-usa/features/order-fulfillment-status-per-line.md +176 -104
- package/knowledge/clients/compass-usa/profile.md +15 -8
- package/package.json +1 -1
package/knowledge/2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
CHANGED
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
@@ -47,8 +47,11 @@ This makes the *picked/packed* portion of the fulfillment lifecycle visible (pre
|
|
|
47
47
|
- **Compass USA + Compass Canada:** all IF stages are *imported*, but order status counts
|
|
48
48
|
**shipped only** — picked/packed never advance a Compass order's status. Compass walks
|
|
49
49
|
Pending → Partially Fulfilled → Fulfilled by shipped quantity alone. **The shipped-only *stage*
|
|
50
|
-
policy is shared, but
|
|
51
|
-
the fulfillment branch lives in `_fulfillmentStatus()
|
|
50
|
+
policy is shared, but the fulfilled-vs-partiallyFulfilled *rule* is now per region.** **Only Canada
|
|
51
|
+
actually changed:** the fulfillment branch lives in `_fulfillmentStatus()`, Canada overrides it with
|
|
52
|
+
a pure per-line rule, and **Compass USA still runs the original order-total logic on purpose**
|
|
53
|
+
(its ASN feed is not trustworthy at line level). The seam was reverted in git on 2026-08-26 and
|
|
54
|
+
rebuilt uncommitted. See
|
|
52
55
|
[Compass USA](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md) and
|
|
53
56
|
[Compass Canada](../../../clients/compass-canada/features/order-fulfillment-status-per-line.md).
|
|
54
57
|
|
|
@@ -67,7 +70,8 @@ shipped-only for ALL clients** — picked/packed quantities never count as inven
|
|
|
67
70
|
`COUNT` and the shipped-qty `SUM` subqueries `INNER JOIN ItemFulfillmentStages →
|
|
68
71
|
ItemFulfillmentStatuses` filtered to the shipped status slug. Shared by Compass USA + Canada.
|
|
69
72
|
Its **fulfillment half is a separate `protected static _fulfillmentStatus()`** called via
|
|
70
|
-
`static::` — that is the per-region seam (see the Compass docs above)
|
|
73
|
+
`static::` — that is the per-region seam (see the Compass docs above), and its **body is the original
|
|
74
|
+
order-total logic that USA still runs**. The approval gate stays in
|
|
71
75
|
`_status()` and is region-independent.
|
|
72
76
|
- **`Model/Compass/Canada/SalesOrder.php`** — Canada's `_fulfillmentStatus()` override (per-line,
|
|
73
77
|
ASN-bridge only).
|
|
@@ -177,11 +181,15 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
|
|
|
177
181
|
the Compass order-lifecycle workflow.
|
|
178
182
|
- **⚠ An order-level "ordered total vs shipped total" comparison is not a fulfillment test.** A
|
|
179
183
|
grand total lets one line's surplus cancel another line's gap, so an order with one line short and
|
|
180
|
-
another over-shipped reads **Fulfilled**. This is
|
|
181
|
-
|
|
182
|
-
`parentSalesOrderItemId IS NULL`, the shipped side
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
another over-shipped reads **Fulfilled**. This is what the Compass `_status` does for **USA
|
|
185
|
+
today** — and its two sides do not even count the same lines (the ordered side filters
|
|
186
|
+
`parentSalesOrderItemId IS NULL`, the shipped side does not). Measured on prod: **90** Canada orders
|
|
187
|
+
were mislabeled and have been corrected by a per-line rule, and **5,223** USA orders would move —
|
|
188
|
+
but the USA change was **deliberately not made**, because duplicate Office Depot ASNs make its
|
|
189
|
+
per-line shipped quantity unreliable. Any client `_status` that judges completeness must compare
|
|
190
|
+
**per line**, and must not do so on a feed that double-credits lines. **⚠ Earlier figures of 5,234
|
|
191
|
+
USA / 115 Canada are void** (the first attempt was reverted in git). See the two Compass docs
|
|
192
|
+
linked above.
|
|
185
193
|
- **A `self::`-qualified call to a calculated field inside the same model always uses the parent's
|
|
186
194
|
logic.** `_Model_Compass_SalesOrder` calls `self::_status()` in its ApprovalDecision notification
|
|
187
195
|
query, so that one call site never sees a region subclass override. Use `static::` unless you
|
|
@@ -191,6 +199,15 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
|
|
|
191
199
|
picked/packed constant would let the literal be removed.
|
|
192
200
|
|
|
193
201
|
## Change history
|
|
202
|
+
- 2026-08-27 — **Corrected the 2026-08-26 entry below: that work was reverted in git (`90c964f1`
|
|
203
|
+
reverted by `43049d1d`) and the "5,234 USA / 115 Canada" figures are void.** Rebuilt state: the
|
|
204
|
+
`_fulfillmentStatus()` per-region seam exists again but is **uncommitted**, `_fulfillmentStatus()`'s
|
|
205
|
+
body is the **original order-total logic that Compass USA still runs deliberately**, and **only
|
|
206
|
+
Compass Canada overrides it** — with a pure per-line rule, no order-total condition, correcting **90**
|
|
207
|
+
prod orders (not 115). A USA per-line rule was measured (5,223 orders would move) and rejected
|
|
208
|
+
because duplicate Office Depot ASNs make USA's per-line shipped quantity unreliable. The
|
|
209
|
+
order-total-is-not-a-fulfillment-test gotcha now records that a per-line test also needs a
|
|
210
|
+
trustworthy line-level feed. (bala)
|
|
194
211
|
- 2026-08-26 — Recorded that the Compass **fulfilled-vs-partiallyFulfilled** rule is now **per
|
|
195
212
|
region**: the fulfillment branch of `_Model_Compass_SalesOrder::_status()` was extracted into a
|
|
196
213
|
`protected static _fulfillmentStatus()` invoked via `static::` (approval gate untouched, emitted
|
|
@@ -6,3 +6,4 @@
|
|
|
6
6
|
| [SAML Downstream Integration Contract](features/downstream-integration-contract.md) | The contract a **downstream TOGa app** implements to authenticate users through the SAML gateway (`saml.togahub.com`). | saml/Controller/Index.php, _underscore/Model/Core/ClientAuthentication.php, _underscore/Model/True/ClientAuthentication.php |
|
|
7
7
|
| [Rate SAML User Provisioning](features/rate-user-provisioning.md) | When a Rate user authenticates via SSO, `_Model_Rate_ClientAuthentication::getAuthenticatedSsoUser()` is called by the saml gateway. | _underscore/Model/Rate/ClientAuthentication.php, _underscore/Model/Rate/User.php, _underscore/Model/Rate/Customer.php, _underscore/Model/Rate/Contact.php |
|
|
8
8
|
| [Onboarding a New SSO Client with Just-In-Time User Provisioning](workflows/onboarding-a-new-sso-client.md) | The repeatable procedure for adding a new client to the 2.0 `saml` gateway when that client's users do **not** pre-exist in our database and must be created on | saml/Controller/Index.php, _underscore/Model/Core/ClientAuthentication.php, _underscore/Model/Rate/ClientAuthentication.php, _underscore/Database.php |
|
|
9
|
+
| [Rotating the SAML SP Certificate](workflows/rotating-the-saml-sp-certificate.md) | The repeatable procedure for replacing the TOGa **SAML Service Provider (SP) credential** — the X.509 cert + private key at `_underscore/Assets/ssl/togahub_com. | _underscore/Assets/ssl/togahub_com.crt, _underscore/Assets/ssl/togahub_private_key.key, _underscore/Model/Core/ClientAuthentication.php, saml/Controller/Index.php |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Rotating the SAML SP Certificate
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: saml
|
|
5
|
+
project: SAML SSO Gateway
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-27
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Assets/ssl/togahub_com.crt
|
|
13
|
+
- _underscore/Assets/ssl/togahub_private_key.key
|
|
14
|
+
- _underscore/Model/Core/ClientAuthentication.php
|
|
15
|
+
- saml/Controller/Index.php
|
|
16
|
+
related:
|
|
17
|
+
- 2.0/apps/saml/architecture.md
|
|
18
|
+
- 2.0/apps/saml/workflows/onboarding-a-new-sso-client.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
The repeatable procedure for replacing the TOGa **SAML Service Provider (SP) credential** — the
|
|
24
|
+
X.509 cert + private key at `_underscore/Assets/ssl/togahub_com.crt` and
|
|
25
|
+
`_underscore/Assets/ssl/togahub_private_key.key`. This credential is the SP's own identity, used
|
|
26
|
+
to **sign outgoing AuthnRequests** and to **decrypt inbound assertions**. It is a bilateral,
|
|
27
|
+
partner-pinned trust artifact, **not** the TLS cert that terminates browser HTTPS. There was no
|
|
28
|
+
rotation runbook before this (git shows two prior undocumented swaps); this closes that gap.
|
|
29
|
+
|
|
30
|
+
For *why* a self-signed, long-lived cert is the correct choice here (rather than a public-CA TLS
|
|
31
|
+
cert), see the SSL certificate trust-model strategy standard.
|
|
32
|
+
|
|
33
|
+
## How the SP credential is used
|
|
34
|
+
|
|
35
|
+
- **Private key — signs outgoing SP AuthnRequests.** `_Model_Core_ClientAuthentication`
|
|
36
|
+
(`_underscore/Model/Core/ClientAuthentication.php`) points its `PATH_TO_PRIVATE_KEY` constant
|
|
37
|
+
at `_underscore/Assets/ssl/togahub_private_key.key` and signs with XMLSecLibs `RSA_SHA256`
|
|
38
|
+
(around lines 11 / 95 / 107).
|
|
39
|
+
- **Public cert — published in SP metadata and used to decrypt assertions.** The `saml` repo's
|
|
40
|
+
`Controller/Index.php` uses the cert for assertion decryption; the `/meta` route publishes it
|
|
41
|
+
in SP metadata via `file_get_contents` read fresh on each request (no server-side cache).
|
|
42
|
+
- **IdPs cache metadata up to 7 days** (`cacheDuration PT604800S`), so a cert change is not
|
|
43
|
+
guaranteed to be picked up by every IdP immediately after deploy.
|
|
44
|
+
|
|
45
|
+
## Current credential (as of 2026-08-27, staged — cutover pending)
|
|
46
|
+
|
|
47
|
+
A dedicated **self-signed** replacement was generated to decouple the SP credential from the
|
|
48
|
+
short-lived DigiCert/Thawte TLS wildcard that had been reused as the SP cert (see Background):
|
|
49
|
+
|
|
50
|
+
- Self-signed X.509, RSA 2048, SHA-256.
|
|
51
|
+
- Subject/Issuer: `C=US, ST=Illinois, L=Naperville, O=TOGA Technology, Inc., CN=togahub.com`
|
|
52
|
+
(reflects the new company name TOGA Technology, Inc. / Naperville, IL — formerly Agilant
|
|
53
|
+
Solutions, Inc.). CN only, **no SANs** — SANs are meaningless for bilateral SAML/AS2 trust.
|
|
54
|
+
- Validity: **2026-08-27 → 2036-08-27** (10 years). Capped at 10y because RSA 2048 is
|
|
55
|
+
NIST-assured only to ~2030; a longer term would call for RSA 4096, and client compatibility
|
|
56
|
+
with 4096 is unconfirmed.
|
|
57
|
+
- SHA-256 fingerprint:
|
|
58
|
+
`0E:1E:9E:11:35:B9:74:79:6F:07:2B:1F:E7:05:B8:B6:E9:07:00:0B:9E:A2:36:94:04:F6:AC:65:AC:B0:03:78`
|
|
59
|
+
|
|
60
|
+
### Where the material lives (never paste the key)
|
|
61
|
+
|
|
62
|
+
- **AWS Secrets Manager**, account **654654170868**, region **us-east-1**.
|
|
63
|
+
- Secret name: **`toga/ssl/togahub_com-selfsigned-2036`**.
|
|
64
|
+
- ARN: `arn:aws:secretsmanager:us-east-1:654654170868:secret:toga/ssl/togahub_com-selfsigned-2036-S2r3SR`.
|
|
65
|
+
- JSON keys: `{certificate, privateKey}`. Retrieve the private key from here at cutover — it is
|
|
66
|
+
**not** stored anywhere else and must never be committed or pasted into chat/docs.
|
|
67
|
+
- The public cert only was staged locally at `C:\TEMP\toga-ssl-2026\togahub_com.crt`; the local
|
|
68
|
+
private key was deleted.
|
|
69
|
+
|
|
70
|
+
## Cutover procedure
|
|
71
|
+
|
|
72
|
+
1. **PRE-CUTOVER SAFETY CHECK — confirm this cert is not serving live TLS.** Before replacing a
|
|
73
|
+
real CA cert with a self-signed one, verify the `.crt`/`.key` at `_underscore/Assets/ssl/` is
|
|
74
|
+
**not** also terminating browser/API TLS anywhere (check the `saml` repo + its EB/Apache
|
|
75
|
+
config). TLS is normally ACM-terminated at the ALB, so this repo file should only be the
|
|
76
|
+
SAML/AS2 credential — but verify against the checked-out `saml` repo first.
|
|
77
|
+
2. **Distribute the new public cert to every pinning partner** (SAML IdPs, AS2 partners, any
|
|
78
|
+
PunchOut buyer that pins our leaf) and let them import it. Include the SHA-256 fingerprint
|
|
79
|
+
above as the verification line.
|
|
80
|
+
3. **Coordinate a cutover date/time** with those partners.
|
|
81
|
+
4. **Replace both files in `_underscore` on the `_production` branch** — via a feature branch +
|
|
82
|
+
PR, **never a direct commit to `_production`**. No code change is needed: the path constants
|
|
83
|
+
already point at these filenames.
|
|
84
|
+
5. **Redeploy `saml` after the `_underscore` merge.** `saml` pulls `_underscore` from
|
|
85
|
+
`_production` at EB build time (`.ebextensions/git.php` + prebuild hook), so a saml-only
|
|
86
|
+
deploy will **not** pick up the new cert. Deploy target: `saml-production` EB env, us-east-1,
|
|
87
|
+
DNS `saml.togahub.com`.
|
|
88
|
+
6. **Deploy the cert+key to the other bilateral services** that use the same SP credential —
|
|
89
|
+
AS2 (AWS Transfer Family) and any other pinned endpoint — importing both cert and key.
|
|
90
|
+
|
|
91
|
+
## Background — why the change
|
|
92
|
+
|
|
93
|
+
The old `_underscore/Assets/ssl/togahub_com.crt` was a **reused DigiCert/Thawte multi-domain
|
|
94
|
+
wildcard TLS cert** (issuer "Thawte TLS RSA CA G1"; SANs for togahub.com, togacommerce.com,
|
|
95
|
+
togadesk.com, togaretail.com, togasupply.com, togaview.com, togaiq.com plus `*.` of each; RSA
|
|
96
|
+
2048; validity Mar 2 2026 → Sep 16 2026, ~6.5 months). Reusing a short-lived public-CA TLS cert
|
|
97
|
+
as the SAML SP credential accidentally coupled SAML to a ~6-month renewal treadmill and forced
|
|
98
|
+
every pinning partner to reinstall on each renewal. The dedicated self-signed 10-year cert
|
|
99
|
+
**decouples the SP credential from TLS renewals** — that is the whole point of the change.
|
|
100
|
+
|
|
101
|
+
## Change history
|
|
102
|
+
- 2026-08-27 — Created while replacing the SAML SP credential. Documented the SP cert's dual use
|
|
103
|
+
(signing AuthnRequests / decrypting assertions), the new self-signed 10-year cert identity and
|
|
104
|
+
its AWS Secrets Manager location, the pre-cutover TLS-usage safety check, and the
|
|
105
|
+
`_underscore`-then-`saml` cutover ordering. Discovered the prior cert was a reused short-lived
|
|
106
|
+
DigiCert wildcard TLS cert, which is the reason for the switch. Cutover itself still pending.
|
|
107
|
+
(jcardinal)
|
|
108
|
+
</content>
|
|
109
|
+
</invoke>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SSL Certificate Trust-Model Strategy
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: shared
|
|
6
|
+
type: standard
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-08-27
|
|
9
|
+
owners: [jcardinal]
|
|
10
|
+
files: []
|
|
11
|
+
related:
|
|
12
|
+
- ../apps/saml/workflows/rotating-the-saml-sp-certificate.md
|
|
13
|
+
- ../apps/saml/architecture.md
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Summary
|
|
17
|
+
|
|
18
|
+
Which kind of SSL/TLS certificate to use is decided by the **trust model** of the endpoint, not
|
|
19
|
+
by habit. Pick wrong and you land on a renewal treadmill; pick right and renewals become either
|
|
20
|
+
invisible or irrelevant. This standard is platform-wide (1.0, 2.0, standalone, and pure-AWS
|
|
21
|
+
infra). It was established while replacing the 2.0 SAML SP credential — see the
|
|
22
|
+
[SAML SP certificate rotation runbook](../apps/saml/workflows/rotating-the-saml-sp-certificate.md).
|
|
23
|
+
|
|
24
|
+
**Critical rule:** if a partner installs/pins OUR specific cert, use a **self-signed, long-lived**
|
|
25
|
+
cert. If AWS terminates the TLS for browsers/generic clients, use **ACM public, auto-renewing**.
|
|
26
|
+
Never reuse a public-CA TLS cert as a pinned bilateral credential.
|
|
27
|
+
|
|
28
|
+
## The decision
|
|
29
|
+
|
|
30
|
+
### 1. Bilateral / pinned trust → self-signed, long-lived
|
|
31
|
+
The partner installs and pins our specific certificate: SAML SP credential, AS2 (AWS Transfer
|
|
32
|
+
Family), PunchOut endpoints a buyer pins. Use a **self-signed** cert with a multi-year validity
|
|
33
|
+
(we generate 10-year). Distribute the public cert to the partner; import cert + key into the
|
|
34
|
+
service. Because trust is by pinning, no public CA is involved and validity can be long.
|
|
35
|
+
|
|
36
|
+
### 2. Standard HTTPS, AWS-terminated → ACM public, non-exportable, auto-renewing
|
|
37
|
+
Browsers / generic clients reach the endpoint through an ALB, CloudFront, or API Gateway that AWS
|
|
38
|
+
terminates. Use an **ACM public certificate** (non-exportable, auto-renewing). Distribute nothing;
|
|
39
|
+
renewal is invisible.
|
|
40
|
+
|
|
41
|
+
### 3. Standard HTTPS, NOT AWS-terminated → ACM exportable, or move behind an ALB
|
|
42
|
+
A server terminates its own TLS (e.g. the legacy Vision box). Options: an **ACM *exportable*
|
|
43
|
+
public cert** (a real 2025 AWS feature) — but still ~13-month validity, so you must redeploy on
|
|
44
|
+
every renewal — OR move the endpoint behind an ALB to get free auto-renew (option 2).
|
|
45
|
+
|
|
46
|
+
## Why long-lived ⇒ self-signed
|
|
47
|
+
|
|
48
|
+
**Every publicly-trusted cert, exportable or not, is capped at ~398 days** by the CA/Browser
|
|
49
|
+
Forum, dropping toward ~47 days by 2029. Only **self-signed / private-CA** certs can be
|
|
50
|
+
multi-year. So "long-lived AND distributable" necessarily means self-signed (or AWS Private CA at
|
|
51
|
+
~$400/mo — rejected here as overkill).
|
|
52
|
+
|
|
53
|
+
The pain of "shrinking public-cert validity" is really a **distribution problem**: automate
|
|
54
|
+
distribution via ACM and it disappears; a partner who pins the leaf breaks on every rotation.
|
|
55
|
+
That is exactly why bilateral, pinned credentials should be self-signed and long-lived — and why
|
|
56
|
+
reusing a short-lived public-CA TLS cert as a pinned SP/AS2 credential (as we accidentally did for
|
|
57
|
+
SAML) couples an otherwise stable credential to a ~6-month treadmill.
|
|
58
|
+
|
|
59
|
+
## Key sizing
|
|
60
|
+
|
|
61
|
+
Self-signed long-lived certs use RSA 2048 / SHA-256 with validity capped at ~10 years, because
|
|
62
|
+
RSA 2048 is NIST-assured only to ~2030. A longer term would want RSA 4096, but partner
|
|
63
|
+
compatibility with 4096 is often unconfirmed — verify before choosing it.
|
|
64
|
+
|
|
65
|
+
## Worked example — Vision / Canon cXML PunchOut (a split-trust endpoint)
|
|
66
|
+
|
|
67
|
+
The Vision cXML PunchOut endpoint (`vision.asisystem.com`, a separate domain/cert from
|
|
68
|
+
togahub.com) is a single cert serving **two legs with different trust models**, which is why its
|
|
69
|
+
renewals are painful:
|
|
70
|
+
|
|
71
|
+
- **Server-to-server cXML leg** — auth is a `<Credential>` **SharedSecret** (localVerifyIdentity),
|
|
72
|
+
not a client cert; our cert there is only the endpoint's TLS wrapper. The buyer reinstalling our
|
|
73
|
+
cert on every renewal indicates they **pin our leaf** — effectively bilateral trust → a
|
|
74
|
+
self-signed long-lived cert fits.
|
|
75
|
+
- **PunchOut portal StartPage** (`punchoutSetupResponseUrlPrefix`) is a real **browser** session →
|
|
76
|
+
needs a public-CA cert (option 2).
|
|
77
|
+
|
|
78
|
+
Bundling both on one cert is the root of the pain. The fix is to **split** them: ACM for the
|
|
79
|
+
browser portal, self-signed for the pinned cXML endpoint. StartPage host is per-identity
|
|
80
|
+
configurable, so the split is feasible. Canon is slated to move from Vision to TOGa Commerce (2.0,
|
|
81
|
+
already ALB+ACM), which auto-solves the browser leg.
|
|
82
|
+
</content>
|
package/knowledge/INDEX.md
CHANGED
|
@@ -23,7 +23,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 13 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
|
-
- **saml** (SAML SSO Gateway) —
|
|
26
|
+
- **saml** (SAML SSO Gateway) — 5 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
28
28
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
29
29
|
- **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: compass-canada
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
@@ -25,43 +25,62 @@ TOGa Supply showed **Fulfilled** on Compass Canada orders that were only partly
|
|
|
25
25
|
`SAC100664` (`SalesOrders.id 665`): line 1 the ZBOOK `C40QYUC#ABA` ordered 1 / shipped 0, line 2 the
|
|
26
26
|
mouse `9VA80AA#ABA` ordered 1 / shipped 1, and the badge read **Fulfilled**.
|
|
27
27
|
|
|
28
|
-
Root cause
|
|
29
|
-
**order-level grand totals** whose two sides did not count the
|
|
30
|
-
`SalesOrderItems.parentSalesOrderItemId IS NULL` (dropping
|
|
31
|
-
summed **every** ASN quantity on the order with no such
|
|
32
|
-
of the ZBOOK (`parentSalesOrderItemId = 1131`), so
|
|
33
|
-
Confirmed by running the exact expression on prod:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
>
|
|
28
|
+
Root cause is in the shared parent `_Model_Compass_SalesOrder`, which decided fulfilled vs.
|
|
29
|
+
partiallyFulfilled by comparing two **order-level grand totals** whose two sides did not count the
|
|
30
|
+
same lines: the ordered side filtered `SalesOrderItems.parentSalesOrderItemId IS NULL` (dropping
|
|
31
|
+
bundle child lines) while the shipped side summed **every** ASN quantity on the order with no such
|
|
32
|
+
filter. On `SAC100664` the mouse is a child of the ZBOOK (`parentSalesOrderItemId = 1131`), so
|
|
33
|
+
ordered = 1 and shipped = 1 and it read fulfilled. Confirmed by running the exact expression on prod:
|
|
34
|
+
orderedSide `1.00`, shippedSide `1`.
|
|
35
|
+
|
|
36
|
+
Canada's fix is a **pure per-line rule** in its own subclass, with **no order-total check at all**.
|
|
37
|
+
Measured on prod after the rule: **489 fulfilled / 184 pendingFulfillment / 109 partiallyFulfilled**,
|
|
38
|
+
which is **90 orders corrected** from Fulfilled to Partially Fulfilled, plus 3 corrected from
|
|
39
|
+
partiallyFulfilled to pendingFulfillment.
|
|
40
|
+
|
|
41
|
+
> **⚠ Read this before trusting any older note.** An earlier version of this work was **reverted in
|
|
42
|
+
> git** and its published numbers were wrong — see *History: the first attempt was reverted* below.
|
|
43
|
+
> The rule described on this page was rebuilt from scratch and is **uncommitted working-tree code**.
|
|
44
|
+
|
|
45
|
+
> **Canada's rule is NOT the USA rule, and the two must not be merged.** Compass **USA is
|
|
46
|
+
> deliberately unchanged** and still runs the order-total logic, because the Office Depot ASN feed is
|
|
47
|
+
> not trustworthy at line level. See
|
|
42
48
|
> [the USA doc](../../compass-usa/features/order-fulfillment-status-per-line.md).
|
|
43
49
|
|
|
44
50
|
## Key files / entry points
|
|
45
51
|
|
|
46
52
|
- **`_underscore/Model/Compass/Canada/SalesOrder.php`** — `_fulfillmentStatus()`, a `protected static`
|
|
47
|
-
override
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
override. **Self-contained:** it builds its own expected-line set and its own per-line shipped
|
|
54
|
+
quantity and borrows nothing conditional from the parent. This is the whole Canada rule.
|
|
55
|
+
- **`_underscore/Model/Compass/SalesOrder.php`** — the parent. The fulfillment branch of `_status()`
|
|
56
|
+
was extracted into `protected static _fulfillmentStatus()` and is called via `static::`, which is
|
|
57
|
+
what makes the Canada override reachable. **The parent body is the ORIGINAL order-total logic,
|
|
58
|
+
moved verbatim** — the seam exists only so Canada can override. Proved behaviour-neutral by
|
|
59
|
+
diffing the emitted `_status` SQL: whitespace-identical, **7,548 normalized characters** both ways.
|
|
60
|
+
- `const ITEM_TYPES__NOT_SHIPPABLE` (Canada) — `SERVICE`, `SERVICES`, `CONSULTING`, `WARRANTY`.
|
|
51
61
|
|
|
52
62
|
## How it works
|
|
53
63
|
|
|
54
64
|
```
|
|
55
|
-
pendingFulfillment WHEN
|
|
56
|
-
partiallyFulfilled WHEN any line
|
|
65
|
+
pendingFulfillment WHEN every expected-to-ship line has received ZERO quantity
|
|
66
|
+
partiallyFulfilled WHEN any expected-to-ship line has received LESS than its ordered quantity
|
|
57
67
|
fulfilled otherwise
|
|
58
68
|
```
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
**Both branches read the same expected-line set and the same per-line quantity**, so the gate and
|
|
71
|
+
the shortfall test can never disagree. That symmetry is the design; keep it if you edit the method.
|
|
72
|
+
|
|
73
|
+
### "Expected to ship" — all four conditions
|
|
74
|
+
|
|
75
|
+
A `SalesOrderItems` row counts only when:
|
|
76
|
+
|
|
77
|
+
1. `price > 0`;
|
|
78
|
+
2. it has a `SalesOrderItems_PurchaseOrderItems` link (it was actually purchased);
|
|
79
|
+
3. its `ItemTypes.name` is **not** in `ITEM_TYPES__NOT_SHIPPABLE`; and
|
|
80
|
+
4. `COALESCE(Items.isFulfillable, 1) = 1`.
|
|
81
|
+
|
|
82
|
+
No parent/child line filter, so bundle children are judged on their own merits — that is the
|
|
83
|
+
original fix.
|
|
65
84
|
|
|
66
85
|
### The per-line shipped source (ASN bridge, and only the ASN bridge)
|
|
67
86
|
|
|
@@ -74,57 +93,136 @@ AdvanceShippingNoticeItems.purchaseOrderItemId
|
|
|
74
93
|
This mapping is **100% complete in `Client_CompassCanada`**: all **947** ASN items have a
|
|
75
94
|
`purchaseOrderItemId` and every one resolves to a sales-order line. Both join columns are indexed.
|
|
76
95
|
Canada ships through Grand & Toy ASNs only (see
|
|
77
|
-
[G&T ASN Import](grand-and-toy-asn-import.md)), so this single source is sufficient
|
|
78
|
-
|
|
96
|
+
[G&T ASN Import](grand-and-toy-asn-import.md)), so this single source is sufficient — Canada needs
|
|
97
|
+
none of USA's TOGa Tech / Office Depot fulfillment chain.
|
|
98
|
+
|
|
99
|
+
## Why each condition is there (measured, prod)
|
|
100
|
+
|
|
101
|
+
- **Condition 3 (item type) is what corrected the over-correction.** Canada has SERVICE and
|
|
102
|
+
CONSULTING lines that **do carry PO links but never ship**: `COMPASS-IPAD-ABM` (Apple Business
|
|
103
|
+
Manager enrolment — 53 lines, only 9 ever on an ASN), `SDU42Z/A` and `SDRU2Z/A` (AppleCare for
|
|
104
|
+
Enterprise), `CN-COMP-DED-IN` / `CN-CONE-SHR-WS1` (Profile SKUs), and `COMPASS-MACBOOK-ABM-ACE`.
|
|
105
|
+
Requiring them to ship held **25 fully-delivered orders** at partiallyFulfilled — e.g. `SAC100041`,
|
|
106
|
+
`SAC100088`, `SAC100523`. Excluding the item types fixed all 25. **This is the 115 to 90 correction.**
|
|
107
|
+
- **Condition 2 (purchased line) is still required.** Canada has **100 priced lines with no PO link
|
|
108
|
+
across 48 orders**; without the link test those lines can never ship and would strand their orders.
|
|
109
|
+
- **Condition 4 (`isFulfillable`) is a no-op in Canada today.** `Items.isFulfillable` is **NULL on
|
|
110
|
+
every Canada item**, so the `COALESCE(...,1)` always passes. It is in the expression so the rule
|
|
111
|
+
starts honouring the flag automatically once Canada is stamped — the **item-type list does all the
|
|
112
|
+
real work right now**. See
|
|
113
|
+
[isFulfillable — Data Quality & the Type-Derived Rule](../../compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
|
|
114
|
+
- **`ItemTypes` is the only usable column in Canada.** Canada's `AssetTypes` are only
|
|
115
|
+
`Equipment` / `Hardware` / `Tablets` — **there is no `FEE` asset type in Canada at all**, so
|
|
116
|
+
`AssetTypes` gives zero signal here. USA is the opposite case; see the USA doc.
|
|
117
|
+
- **Keep the `price > 0` filter.** Only **9** of Canada's **69** zero-price lines ever receive an
|
|
118
|
+
ASN, so requiring them to ship would strand orders in partiallyFulfilled forever.
|
|
119
|
+
|
|
120
|
+
## Why per-line QUANTITY is the right unit (the counter-examples)
|
|
121
|
+
|
|
122
|
+
- **Order totals fail.** `SAC100664`: the mouse's shipment paid for the ZBOOK's quantity.
|
|
123
|
+
- **Line counts fail.** `SAC100108`: both expected lines had an ASN, so a line-count check matched —
|
|
124
|
+
but only 1 of 2 iPads and 1 of 2 accessories actually shipped. Also **96 Canada lines carry 2 to 3
|
|
125
|
+
ASN lines each**, so an ASN-line count can exceed the order-line count while the order is still
|
|
126
|
+
short.
|
|
127
|
+
- **There is no line-coverage gap to catch.** Across **601** Canada orders past approval with
|
|
128
|
+
shipments, **zero** read fulfilled while an expected line had no ASN line — a line with no ASN has
|
|
129
|
+
shipped 0 and is always caught as short. A line-count check would add nothing and only weaken the
|
|
130
|
+
rule.
|
|
131
|
+
- **Going per line also neutralises duplicate ASNs for the verdict.** A surplus stays on its own line
|
|
132
|
+
and can no longer cover another line's gap, so no clamping or de-duplication is needed.
|
|
133
|
+
|
|
134
|
+
## The pendingFulfillment gate reads quantity, not "an ASN exists"
|
|
135
|
+
|
|
136
|
+
The gate is **not** `COUNT(ASNs on the order) = 0`. Three orders (`SAC100131`, `SAC100509`,
|
|
137
|
+
`SAC100522`) had an ASN raised only against an AppleCare / ABM line while **zero physical goods had
|
|
138
|
+
shipped**, and therefore read partiallyFulfilled. Reading the expected-line quantity instead makes
|
|
139
|
+
them correctly read **pendingFulfillment**.
|
|
79
140
|
|
|
80
141
|
## Design decisions worth keeping
|
|
81
142
|
|
|
82
143
|
- **⚠ Do NOT switch Canada to the local `ItemFulfillments` / `ItemFulfillmentItems` tables**, even
|
|
83
144
|
though the line-level `_qtyFulfilled` UI column does read them. They are **incomplete** in Canada:
|
|
84
145
|
**47** orders have ASN shipments with no local `ItemFulfillment` at all and **52** more disagree on
|
|
85
|
-
quantity, so moving
|
|
86
|
-
|
|
87
|
-
- **
|
|
88
|
-
|
|
146
|
+
quantity, so moving this rule onto the base `_Model_Client_SalesOrder` logic would regress roughly
|
|
147
|
+
**99** orders.
|
|
148
|
+
- **Canada deliberately has NO order-total condition.** USA keeps one; Canada does not need it,
|
|
149
|
+
because its ASN bridge is complete and its expected-line set is never empty in the way USA's is.
|
|
89
150
|
- **Canada deliberately omits any Compass-vendor exclusion.** `VENDOR_ID__COMPASS = 26` on the parent
|
|
90
151
|
is the **US** Compass vendor id. In `Client_CompassCanada` all **901** POs are GRAND & TOY
|
|
91
|
-
(`vendorId 1`), and "COMPASS CANADA" is `vendorId 4` with **zero** POs —
|
|
92
|
-
|
|
93
|
-
- **Canada needs no purchased-line filter** (unlike USA) precisely because the ASN bridge is complete
|
|
94
|
-
and the vendor set is a single vendor.
|
|
152
|
+
(`vendorId 1`), and "COMPASS CANADA" is `vendorId 4` with **zero** POs — the inherited constant is
|
|
153
|
+
a latent wrong-id no-op. Do not "restore" it here.
|
|
95
154
|
|
|
96
155
|
## Gotchas / known issues
|
|
97
156
|
|
|
157
|
+
- **⚠ Uncommitted.** Both touched files pass `php -l`, nothing was committed or pushed, and the repo
|
|
158
|
+
is sitting on **`_production`** — a branch is required before committing. Canada has **not** been
|
|
159
|
+
exercised through the Supply UI yet.
|
|
98
160
|
- **The badge is display-only and never stored.** The status is computed live, so there is no
|
|
99
|
-
backfill and reverting the code fully reverts the behaviour.
|
|
161
|
+
backfill and reverting the code fully reverts the behaviour. **No cron, email or report reads the
|
|
162
|
+
fulfilled vs partiallyFulfilled distinction.** Canada's in-transit email cron
|
|
100
163
|
(`worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php`) keeps its **own
|
|
101
164
|
duplicated copy** of the `_status` CASE, but it only ever produces canceled /
|
|
102
165
|
pendingApprovalUnknown / pendingFulfillment / shipped and filters on
|
|
103
|
-
`computedStatusSlug = 'shipped'
|
|
104
|
-
|
|
166
|
+
`computedStatusSlug = 'shipped'`. Note the Canada pendingFulfillment gate is **no longer
|
|
167
|
+
byte-identical** to that copy (it now reads quantity, not ASN existence). The cron's copy is
|
|
168
|
+
unchanged and still only acts on `'shipped'`, so behaviour is unaffected, but the two are now a
|
|
169
|
+
genuine divergence.
|
|
105
170
|
- **⚠ Canada's order status is no longer identical to Compass USA.** Older notes (and the profile)
|
|
106
171
|
said "order status is shipped-only, same as Compass USA". The *stage* policy is still shared
|
|
107
|
-
(shipped-only), but the **fulfilled vs partiallyFulfilled rule now diverges by region
|
|
172
|
+
(shipped-only), but the **fulfilled vs partiallyFulfilled rule now diverges by region** — and it
|
|
173
|
+
diverges because **only Canada changed**.
|
|
108
174
|
- **The ApprovalDecision notification query on the parent calls `self::_status()`**, not `static::`,
|
|
109
|
-
so it always evaluates the **parent** fulfillment rule even for Canada.
|
|
110
|
-
|
|
111
|
-
|
|
175
|
+
so it always evaluates the **parent** fulfillment rule even for Canada. Because Canada's
|
|
176
|
+
pendingFulfillment gate now differs from the parent's, that query sees the **parent's** gate for
|
|
177
|
+
Canada orders. Harmless today (it only branches on pendingApproval / canceled / pendingFulfillment
|
|
178
|
+
and acts on approvals) but it is now a real divergence, not a theoretical one.
|
|
179
|
+
- **Canada's approval gate is healthy**, unlike USA's: 733 approved, 30 denied, only **19** stuck at a
|
|
180
|
+
pending-approval status. Canada also has all ten `SalesOrderStages` rows, where USA has only ids
|
|
181
|
+
5 to 9.
|
|
182
|
+
|
|
183
|
+
## History: the first attempt was reverted
|
|
184
|
+
|
|
185
|
+
- The original fix, commit **`90c964f1`** "Updating the status logic as per the client", was
|
|
186
|
+
**reverted by commit `43049d1d`** ("Revert …", kmaramreddy08, 2026-08-26 16:19). The revert is on
|
|
187
|
+
**`#sprint85`, `TRUE-81284`, `_beta`, `_production` and `_sandbox-client`**; only **`_sandbox-dev`**
|
|
188
|
+
still carries the original fix.
|
|
189
|
+
- Every refinement made after `90c964f1` was **never committed** and is gone from git. The rule on
|
|
190
|
+
this page was therefore **rebuilt from scratch** on `_production`.
|
|
191
|
+
- The ticket branch for this work is **`TRUE-81284`**, not `TRUE-80431`.
|
|
192
|
+
- **Any statement that this shipped is wrong**, including the previously published "115 Canada
|
|
193
|
+
orders corrected" figure.
|
|
112
194
|
|
|
113
195
|
## Change history
|
|
114
|
-
- 2026-08-
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
|
|
196
|
+
- 2026-08-27 — **Corrects the 2026-08-26 entry below, which described work that was reverted in git
|
|
197
|
+
(`90c964f1` reverted by `43049d1d`) and published a wrong count.** Canada's `_fulfillmentStatus()`
|
|
198
|
+
rebuilt from scratch as a self-contained pure per-line rule with **no order-total check**:
|
|
199
|
+
pendingFulfillment when every expected-to-ship line has received zero, partiallyFulfilled when any
|
|
200
|
+
expected line is short, else fulfilled — both branches reading the same expected-line set and the
|
|
201
|
+
same per-line quantity. "Expected to ship" now also requires `ItemTypes.name NOT IN
|
|
202
|
+
ITEM_TYPES__NOT_SHIPPABLE` (SERVICE/SERVICES/CONSULTING/WARRANTY) and
|
|
203
|
+
`COALESCE(Items.isFulfillable,1) = 1`, which released **25 fully-delivered orders** wrongly held at
|
|
204
|
+
partiallyFulfilled by PO-linked-but-never-shipped AppleCare/ABM/Profile SKUs — so the real impact is
|
|
205
|
+
**90 orders corrected, not 115**. The pendingFulfillment gate now reads expected-line quantity
|
|
206
|
+
instead of "any ASN exists", correcting `SAC100131` / `SAC100509` / `SAC100522`. Prod after:
|
|
207
|
+
489 fulfilled / 184 pendingFulfillment / 109 partiallyFulfilled. Recorded that `isFulfillable` is
|
|
208
|
+
NULL on every Canada item (so the item-type list does the work), that Canada has no `FEE` asset
|
|
209
|
+
type, and why per-line **quantity** beats order totals (`SAC100664`) and line counts (`SAC100108`).
|
|
210
|
+
Uncommitted; repo on `_production`; not yet exercised through the Supply UI. (bala)
|
|
211
|
+
- 2026-08-26 — **REVERTED IN GIT — do not rely on this entry.** Original per-line
|
|
212
|
+
`_fulfillmentStatus()` override for Canada (pendingFulfillment when the order had no ASN at all,
|
|
213
|
+
otherwise partiallyFulfilled if any `price > 0` line was short of its own ASN-bridge quantity),
|
|
214
|
+
reported as 115 orders corrected. Commit `90c964f1` was reverted by `43049d1d` on 2026-08-26; the
|
|
215
|
+
rule over-corrected by 25 orders and its gate tested ASN existence rather than quantity. Superseded
|
|
216
|
+
by the 2026-08-27 entry. Still valid from this work: the ASN bridge is the correct source (100%
|
|
217
|
+
complete, 947 items), the local `ItemFulfillments` tables are not (would regress ~99 orders),
|
|
218
|
+
`price > 0` stays, and the inherited `VENDOR_ID__COMPASS = 26` is a US-only id and a no-op here.
|
|
219
|
+
(bala)
|
|
124
220
|
|
|
125
221
|
## Related docs
|
|
126
|
-
- [Compass USA — order aggregate
|
|
127
|
-
— the
|
|
222
|
+
- [Compass USA — order aggregate, deliberately unchanged](../../compass-usa/features/order-fulfillment-status-per-line.md)
|
|
223
|
+
— the parent rule, and why USA was not touched.
|
|
128
224
|
- [Grand & Toy ASN Import](grand-and-toy-asn-import.md) — where Canada's ASN rows come from.
|
|
129
225
|
- [IF Stage Lifecycle & Order Status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md)
|
|
130
226
|
— the shipped-only stage policy shared by both Compass regions.
|
|
227
|
+
- [FIELD_SQL calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md)
|
|
228
|
+
— why a region-subclass override of a calculated field is picked up at all.
|
|
@@ -16,7 +16,7 @@ project: _Underscore
|
|
|
16
16
|
client: compass-canada
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-08-
|
|
19
|
+
updated: 2026-08-27
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -148,14 +148,20 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
148
148
|
lifecycle stages (picked/packed/shipped) + shipped backfill are seeded by
|
|
149
149
|
`dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql`.
|
|
150
150
|
See [IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
151
|
-
- **⚠ Order status is no longer identical to Compass USA — the
|
|
152
|
-
|
|
153
|
-
`_Model_Compass_Canada_SalesOrder` with a
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
- **⚠ Order status is no longer identical to Compass USA — Canada is the only region whose
|
|
152
|
+
fulfilled-vs-partiallyFulfilled rule changed (2026-08-27).** Canada overrides
|
|
153
|
+
`_fulfillmentStatus()` in `_Model_Compass_Canada_SalesOrder` with a **pure per-line** rule and **no
|
|
154
|
+
order-total condition**, sourced from the **ASN bridge only**
|
|
155
|
+
(`AdvanceShippingNoticeItems.purchaseOrderItemId` -> `SalesOrderItems_PurchaseOrderItems`, 100%
|
|
156
|
+
complete here). A line counts as expected-to-ship only when `price > 0`, it has a PO-item link, its
|
|
157
|
+
`ItemTypes.name` is not in `ITEM_TYPES__NOT_SHIPPABLE` (SERVICE/SERVICES/CONSULTING/WARRANTY) and
|
|
158
|
+
`COALESCE(Items.isFulfillable,1) = 1` — the item-type list matters because Canada has PO-linked
|
|
159
|
+
AppleCare/ABM SKUs that never ship, and `isFulfillable` is NULL on every Canada item. Do not read
|
|
160
|
+
Canada's local `ItemFulfillments` tables for this (incomplete: ~99 orders would regress), and do
|
|
161
|
+
not port USA's rule or its `VENDOR_ID__COMPASS = 26` (a US vendor id; all 901 Canada POs are Grand
|
|
162
|
+
& Toy `vendorId 1`). **Corrects 90 prod orders, not the 115 previously published** — the first
|
|
163
|
+
attempt was reverted in git and over-corrected by 25 orders. **Uncommitted** (repo on
|
|
164
|
+
`_production`). See
|
|
159
165
|
[Fulfilled vs Partially Fulfilled](features/order-fulfillment-status-per-line.md).
|
|
160
166
|
- **Item titles in customer emails** come from the `ItemTranslations` sidecar (prod: 220 fr-CA rows,
|
|
161
167
|
100% coverage of every item ever ordered). Resolve the English/translated fallback **in PHP**, never
|