@mj-biz-apps/sales-entities 5.2.0 → 6.1.0

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.
@@ -17,16 +17,25 @@
17
17
  * at all. So a name- or SKU-matching resolver can be a SUGGESTION a human confirms, never an identity
18
18
  * mechanism, and `DealLine.ProductID` stores the ID.
19
19
  *
20
- * ── THE THREE CONDITIONS ────────────────────────────────────────────────────────────────────────
20
+ * ── THE CONDITIONS ────────────────────────────────────────────────────────────────────────
21
21
  *
22
- * 1. **CompanyID.** Products are per-company. Without this clause a rep on one tenant's pipeline can
23
- * select another tenant's product — the most consequential of the three, because it is a data leak
24
- * rather than a mistake. The company comes from the DEAL'S PIPELINE, which is the same source the
25
- * server stamps `Deal.CompanyID` from, so the picker and the server cannot disagree.
26
- * 2. **Status = 'Active'.** `Draft` is not sellable yet; `Discontinued` and `EOL` are not sellable any
22
+ * There used to be a THIRD condition, and removing it was a business decision rather than a tidy-up.
23
+ *
24
+ * The filter began with `CompanyID = <the deal's company>`, on the reasoning that products are
25
+ * per-company and a rep on one company's pipeline selecting another's product would be a data leak.
26
+ * That reading was wrong for Blue Cypress: every deal is a Blue Cypress deal, and company ownership
27
+ * lives at the PRODUCT rather than at the deal (Johanna Snider, Sales channel, 2026-08-26; issue #29).
28
+ * With both pipelines owned by Blue Cypress the clause made every non-Blue-Cypress product unsellable,
29
+ * so an Account Director could not put a Betty or Sidecar product on a deal at all.
30
+ *
31
+ * It is not a tenancy boundary being relaxed, because there was never one here to relax. `DECISIONS.md`
32
+ * D5 already says a deal lives in ONE company's pipeline while its lines carry their OWN company, taken
33
+ * from the product. The clause contradicted D5; the picker now agrees with it.
34
+ *
35
+ * 1. **Status = 'Active'.** `Draft` is not sellable yet; `Discontinued` and `EOL` are not sellable any
27
36
  * more. Compared as a literal because it is orders' own CHECK-constrained vocabulary, not this app's
28
37
  * — the Sales vocabulary rule governs Sales' type tables, and this column belongs to another app.
29
- * 3. **The availability window.** `AvailableFrom` / `AvailableTo` are nullable and open-ended on either
38
+ * 2. **The availability window.** `AvailableFrom` / `AvailableTo` are nullable and open-ended on either
30
39
  * side, so the test is "not yet started" and "already ended" rather than a range containment. Both
31
40
  * NULL means always available, which is the common case.
32
41
  *
@@ -51,6 +60,28 @@ export interface ProductLookup {
51
60
  ID: string;
52
61
  Name: string;
53
62
  SKU: string | null;
63
+ /**
64
+ * The company that OWNS this product, and so the company its revenue books to.
65
+ *
66
+ * Carried because the picker no longer filters on company, which means a line's company can no
67
+ * longer be inferred from the deal — it has to come from the product the rep actually chose.
68
+ * `OnProductChange` stamps it. Orders' `OrderLineEntityServer` overwrites it from the product at
69
+ * save regardless; this makes the BROWSER agree with that rather than guess, which matters because
70
+ * `deal.Validate()` runs client-side where that server subclass does not exist.
71
+ */
72
+ CompanyID: string;
73
+ /**
74
+ * The owning company's NAME, for display only — never for matching.
75
+ *
76
+ * The picker spans every company since #29, so `Name` alone no longer identifies a product to a
77
+ * reader. Two companies can each sell an "Onboarding Fee", and `SKU` is nullable with only a
78
+ * FILTERED unique index, so neither field disambiguates them. Without the company on the label a
79
+ * rep chooses between two identical-looking rows, and the choice decides which company's books the
80
+ * revenue lands in.
81
+ *
82
+ * It is a virtual field on orders' Products view, so it costs nothing but a column.
83
+ */
84
+ Company: string | null;
54
85
  }
55
86
  /**
56
87
  * The filter that decides what a rep may select, as of `asOf`.
@@ -59,8 +90,7 @@ export interface ProductLookup {
59
90
  * the comment explaining each. The date is passed in rather than read from the clock inside so the
60
91
  * behaviour is testable at a chosen instant.
61
92
  *
62
- * @param companyID - The selling company, from the deal's pipeline.
63
93
  * @param asOf - The date the availability window is judged against. UTC, because everything stored is.
64
94
  */
65
- export declare function ProductFilterFor(companyID: string, asOf: Date): string;
95
+ export declare function ProductFilterFor(asOf: Date): string;
66
96
  //# sourceMappingURL=product-filter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"product-filter.d.ts","sourceRoot":"","sources":["../src/product-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAUtE"}
1
+ {"version":3,"file":"product-filter.d.ts","sourceRoot":"","sources":["../src/product-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;;;;;OAQG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;;;;;;;;OAUG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAOnD"}
@@ -17,16 +17,25 @@
17
17
  * at all. So a name- or SKU-matching resolver can be a SUGGESTION a human confirms, never an identity
18
18
  * mechanism, and `DealLine.ProductID` stores the ID.
19
19
  *
20
- * ── THE THREE CONDITIONS ────────────────────────────────────────────────────────────────────────
20
+ * ── THE CONDITIONS ────────────────────────────────────────────────────────────────────────
21
21
  *
22
- * 1. **CompanyID.** Products are per-company. Without this clause a rep on one tenant's pipeline can
23
- * select another tenant's product — the most consequential of the three, because it is a data leak
24
- * rather than a mistake. The company comes from the DEAL'S PIPELINE, which is the same source the
25
- * server stamps `Deal.CompanyID` from, so the picker and the server cannot disagree.
26
- * 2. **Status = 'Active'.** `Draft` is not sellable yet; `Discontinued` and `EOL` are not sellable any
22
+ * There used to be a THIRD condition, and removing it was a business decision rather than a tidy-up.
23
+ *
24
+ * The filter began with `CompanyID = <the deal's company>`, on the reasoning that products are
25
+ * per-company and a rep on one company's pipeline selecting another's product would be a data leak.
26
+ * That reading was wrong for Blue Cypress: every deal is a Blue Cypress deal, and company ownership
27
+ * lives at the PRODUCT rather than at the deal (Johanna Snider, Sales channel, 2026-08-26; issue #29).
28
+ * With both pipelines owned by Blue Cypress the clause made every non-Blue-Cypress product unsellable,
29
+ * so an Account Director could not put a Betty or Sidecar product on a deal at all.
30
+ *
31
+ * It is not a tenancy boundary being relaxed, because there was never one here to relax. `DECISIONS.md`
32
+ * D5 already says a deal lives in ONE company's pipeline while its lines carry their OWN company, taken
33
+ * from the product. The clause contradicted D5; the picker now agrees with it.
34
+ *
35
+ * 1. **Status = 'Active'.** `Draft` is not sellable yet; `Discontinued` and `EOL` are not sellable any
27
36
  * more. Compared as a literal because it is orders' own CHECK-constrained vocabulary, not this app's
28
37
  * — the Sales vocabulary rule governs Sales' type tables, and this column belongs to another app.
29
- * 3. **The availability window.** `AvailableFrom` / `AvailableTo` are nullable and open-ended on either
38
+ * 2. **The availability window.** `AvailableFrom` / `AvailableTo` are nullable and open-ended on either
30
39
  * side, so the test is "not yet started" and "already ended" rather than a range containment. Both
31
40
  * NULL means always available, which is the common case.
32
41
  *
@@ -48,15 +57,11 @@ export const E_ORDERS_PRODUCT = 'MJ_BizApps_Orders: Products';
48
57
  * the comment explaining each. The date is passed in rather than read from the clock inside so the
49
58
  * behaviour is testable at a chosen instant.
50
59
  *
51
- * @param companyID - The selling company, from the deal's pipeline.
52
60
  * @param asOf - The date the availability window is judged against. UTC, because everything stored is.
53
61
  */
54
- export function ProductFilterFor(companyID, asOf) {
62
+ export function ProductFilterFor(asOf) {
55
63
  const day = `${asOf.getUTCFullYear()}-${String(asOf.getUTCMonth() + 1).padStart(2, '0')}-${String(asOf.getUTCDate()).padStart(2, '0')}`;
56
- // Single-quote escaping: a company ID is a GUID from our own metadata rather than user input, but
57
- // the filter is still concatenated SQL and the habit is what keeps it safe when a caller changes.
58
- const company = companyID.replace(/'/g, "''");
59
- return (`CompanyID = '${company}' AND Status = 'Active' ` +
64
+ return (`Status = 'Active' ` +
60
65
  `AND (AvailableFrom IS NULL OR AvailableFrom <= '${day}') ` +
61
66
  `AND (AvailableTo IS NULL OR AvailableTo >= '${day}')`);
62
67
  }
@@ -1 +1 @@
1
- {"version":3,"file":"product-filter.js","sourceRoot":"","sources":["../src/product-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAc9D;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,IAAU;IAC1D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,cAAc,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACxI,kGAAkG;IAClG,kGAAkG;IAClG,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,CACH,gBAAgB,OAAO,0BAA0B;QACjD,mDAAmD,GAAG,KAAK;QAC3D,+CAA+C,GAAG,IAAI,CACzD,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"product-filter.js","sourceRoot":"","sources":["../src/product-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAqC9D;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAU;IACvC,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,cAAc,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACxI,OAAO,CACH,oBAAoB;QACpB,mDAAmD,GAAG,KAAK;QAC3D,+CAA+C,GAAG,IAAI,CACzD,CAAC;AACN,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mj-biz-apps/sales-entities",
3
3
  "type": "module",
4
- "version": "5.2.0",
4
+ "version": "6.1.0",
5
5
  "description": "MJ BizApps Sales Entity Subclasses",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -18,12 +18,12 @@
18
18
  "typescript": "^5.9.3"
19
19
  },
20
20
  "dependencies": {
21
- "@mj-biz-apps/orders-entities": "^5.0.0",
21
+ "@mj-biz-apps/orders-entities": "^5.2.1",
22
22
  "zod": "~3.24.4"
23
23
  },
24
24
  "peerDependencies": {
25
- "@memberjunction/core": "^6.1.0-edge.3",
26
- "@memberjunction/global": "^6.1.0-edge.3"
25
+ "@memberjunction/core": "^6.1.0-edge.5",
26
+ "@memberjunction/global": "^6.1.0-edge.5"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",