@mj-biz-apps/orders-entities 0.0.1 → 5.2.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.
Files changed (115) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +50 -2
  3. package/dist/CustomerPaymentMethodEntity.d.ts +4 -0
  4. package/dist/CustomerPaymentMethodEntity.d.ts.map +1 -0
  5. package/dist/CustomerPaymentMethodEntity.js +26 -0
  6. package/dist/CustomerPaymentMethodEntity.js.map +1 -0
  7. package/dist/InitialPaymentIntentCompanion.d.ts +29 -0
  8. package/dist/InitialPaymentIntentCompanion.d.ts.map +1 -0
  9. package/dist/InitialPaymentIntentCompanion.js +40 -0
  10. package/dist/InitialPaymentIntentCompanion.js.map +1 -0
  11. package/dist/OrderHeaderEntity.d.ts +224 -0
  12. package/dist/OrderHeaderEntity.d.ts.map +1 -0
  13. package/dist/OrderHeaderEntity.js +509 -0
  14. package/dist/OrderHeaderEntity.js.map +1 -0
  15. package/dist/OrderLineEntity.d.ts +25 -0
  16. package/dist/OrderLineEntity.d.ts.map +1 -0
  17. package/dist/OrderLineEntity.js +54 -0
  18. package/dist/OrderLineEntity.js.map +1 -0
  19. package/dist/OrderLineExtensionCompanion.d.ts +54 -0
  20. package/dist/OrderLineExtensionCompanion.d.ts.map +1 -0
  21. package/dist/OrderLineExtensionCompanion.js +167 -0
  22. package/dist/OrderLineExtensionCompanion.js.map +1 -0
  23. package/dist/OrderStatusBehavior.d.ts +86 -0
  24. package/dist/OrderStatusBehavior.d.ts.map +1 -0
  25. package/dist/OrderStatusBehavior.js +160 -0
  26. package/dist/OrderStatusBehavior.js.map +1 -0
  27. package/dist/PartyAffiliationBehavior.d.ts +20 -0
  28. package/dist/PartyAffiliationBehavior.d.ts.map +1 -0
  29. package/dist/PartyAffiliationBehavior.js +52 -0
  30. package/dist/PartyAffiliationBehavior.js.map +1 -0
  31. package/dist/PaymentHeaderEntity.d.ts +18 -0
  32. package/dist/PaymentHeaderEntity.d.ts.map +1 -0
  33. package/dist/PaymentHeaderEntity.js +80 -0
  34. package/dist/PaymentHeaderEntity.js.map +1 -0
  35. package/dist/PricingDriverResolver.d.ts +70 -0
  36. package/dist/PricingDriverResolver.d.ts.map +1 -0
  37. package/dist/PricingDriverResolver.js +145 -0
  38. package/dist/PricingDriverResolver.js.map +1 -0
  39. package/dist/PromotionCodesCompanion.d.ts +77 -0
  40. package/dist/PromotionCodesCompanion.d.ts.map +1 -0
  41. package/dist/PromotionCodesCompanion.js +111 -0
  42. package/dist/PromotionCodesCompanion.js.map +1 -0
  43. package/dist/booked-money.d.ts +27 -0
  44. package/dist/booked-money.d.ts.map +1 -0
  45. package/dist/booked-money.js +67 -0
  46. package/dist/booked-money.js.map +1 -0
  47. package/dist/configuration-types.d.ts +157 -0
  48. package/dist/configuration-types.d.ts.map +1 -0
  49. package/dist/configuration-types.js +10 -0
  50. package/dist/configuration-types.js.map +1 -0
  51. package/dist/date-cell.d.ts +109 -0
  52. package/dist/date-cell.d.ts.map +1 -0
  53. package/dist/date-cell.js +131 -0
  54. package/dist/date-cell.js.map +1 -0
  55. package/dist/generated/entity_subclasses.d.ts +12055 -0
  56. package/dist/generated/entity_subclasses.d.ts.map +1 -0
  57. package/dist/generated/entity_subclasses.js +17800 -0
  58. package/dist/generated/entity_subclasses.js.map +1 -0
  59. package/dist/generated/remote_operations.d.ts +2365 -0
  60. package/dist/generated/remote_operations.d.ts.map +1 -0
  61. package/dist/generated/remote_operations.js +768 -0
  62. package/dist/generated/remote_operations.js.map +1 -0
  63. package/dist/index.d.ts +102 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +103 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/line-quantity.d.ts +6 -0
  68. package/dist/line-quantity.d.ts.map +1 -0
  69. package/dist/line-quantity.js +11 -0
  70. package/dist/line-quantity.js.map +1 -0
  71. package/dist/overdue.d.ts +77 -0
  72. package/dist/overdue.d.ts.map +1 -0
  73. package/dist/overdue.js +90 -0
  74. package/dist/overdue.js.map +1 -0
  75. package/dist/pricing/ChargeBehavior.d.ts +64 -0
  76. package/dist/pricing/ChargeBehavior.d.ts.map +1 -0
  77. package/dist/pricing/ChargeBehavior.js +124 -0
  78. package/dist/pricing/ChargeBehavior.js.map +1 -0
  79. package/dist/pricing/ChargeEngine.d.ts +54 -0
  80. package/dist/pricing/ChargeEngine.d.ts.map +1 -0
  81. package/dist/pricing/ChargeEngine.js +154 -0
  82. package/dist/pricing/ChargeEngine.js.map +1 -0
  83. package/dist/pricing/OrderPricingService.d.ts +244 -0
  84. package/dist/pricing/OrderPricingService.d.ts.map +1 -0
  85. package/dist/pricing/OrderPricingService.js +516 -0
  86. package/dist/pricing/OrderPricingService.js.map +1 -0
  87. package/dist/pricing/OrdersEngine.d.ts +100 -0
  88. package/dist/pricing/OrdersEngine.d.ts.map +1 -0
  89. package/dist/pricing/OrdersEngine.js +156 -0
  90. package/dist/pricing/OrdersEngine.js.map +1 -0
  91. package/dist/pricing/PriceResolver.d.ts +127 -0
  92. package/dist/pricing/PriceResolver.d.ts.map +1 -0
  93. package/dist/pricing/PriceResolver.js +345 -0
  94. package/dist/pricing/PriceResolver.js.map +1 -0
  95. package/dist/pricing/PricingBehavior.d.ts +161 -0
  96. package/dist/pricing/PricingBehavior.d.ts.map +1 -0
  97. package/dist/pricing/PricingBehavior.js +351 -0
  98. package/dist/pricing/PricingBehavior.js.map +1 -0
  99. package/dist/pricing/PromotionBehavior.d.ts +73 -0
  100. package/dist/pricing/PromotionBehavior.d.ts.map +1 -0
  101. package/dist/pricing/PromotionBehavior.js +166 -0
  102. package/dist/pricing/PromotionBehavior.js.map +1 -0
  103. package/dist/pricing/PromotionEngine.d.ts +131 -0
  104. package/dist/pricing/PromotionEngine.d.ts.map +1 -0
  105. package/dist/pricing/PromotionEngine.js +505 -0
  106. package/dist/pricing/PromotionEngine.js.map +1 -0
  107. package/dist/pricing/TaxResolver.d.ts +162 -0
  108. package/dist/pricing/TaxResolver.d.ts.map +1 -0
  109. package/dist/pricing/TaxResolver.js +361 -0
  110. package/dist/pricing/TaxResolver.js.map +1 -0
  111. package/dist/save-populated-fields.d.ts +17 -0
  112. package/dist/save-populated-fields.d.ts.map +1 -0
  113. package/dist/save-populated-fields.js +25 -0
  114. package/dist/save-populated-fields.js.map +1 -0
  115. package/package.json +34 -5
@@ -0,0 +1,80 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ /**
8
+ * Shared (client + server) subclass for Payment Headers.
9
+ *
10
+ * Declares the optional `PaymentDetailID` embed — the receipt's own frozen
11
+ * instrument snapshot (D39 copy-on-use). `PaymentHeaderEntityServer` extends
12
+ * this class. Remove the declaration when CodeGen emits it from metadata.
13
+ */
14
+ import { BaseEntity, ValidationErrorType } from '@memberjunction/core';
15
+ import { RegisterClass } from '@memberjunction/global';
16
+ import { mjBizAppsOrdersPaymentHeaderEntity, } from './generated/entity_subclasses.js';
17
+ import { IsSavePopulatedFieldError } from './save-populated-fields.js';
18
+ const ENTITY = 'MJ_BizApps_Orders: Payment Headers';
19
+ const DETAIL_ENTITY = 'MJ_BizApps_Orders: Payment Details';
20
+ let PaymentHeaderEntity = class PaymentHeaderEntity extends mjBizAppsOrdersPaymentHeaderEntity {
21
+ ClearPaymentDetail() {
22
+ this.GetCompanion('PaymentDetailID_Object')?.Clear();
23
+ }
24
+ get ReferenceNumber() {
25
+ return this.PaymentDetailID_Object?.ReferenceNumber ?? null;
26
+ }
27
+ set ReferenceNumber(value) {
28
+ const trimmed = (value ?? '').trim();
29
+ const ref = trimmed.length ? trimmed : null;
30
+ if (ref) {
31
+ const detail = this.PaymentDetailID_EnsureObject();
32
+ detail.ReferenceNumber = ref;
33
+ if (this.ReceivingCompanyID && !detail.CompanyID) {
34
+ detail.CompanyID = this.ReceivingCompanyID;
35
+ }
36
+ if (this.PaymentTypeID && !detail.PaymentTypeID) {
37
+ detail.PaymentTypeID = this.PaymentTypeID;
38
+ }
39
+ }
40
+ else if (this.PaymentDetailID_Object) {
41
+ this.PaymentDetailID_Object.ReferenceNumber = null;
42
+ if (this.isPaymentDetailEmpty(this.PaymentDetailID_Object)) {
43
+ this.ClearPaymentDetail();
44
+ }
45
+ }
46
+ }
47
+ isPaymentDetailEmpty(detail) {
48
+ return (!detail.ReferenceNumber?.trim() &&
49
+ !detail.Brand?.trim() &&
50
+ !detail.Last4?.trim() &&
51
+ !detail.HolderName?.trim() &&
52
+ !detail.BankName?.trim() &&
53
+ !detail.RoutingLast4?.trim() &&
54
+ !detail.AccountLast4?.trim() &&
55
+ !detail.BankAccountType &&
56
+ !detail.ExpiryMonth &&
57
+ !detail.ExpiryYear &&
58
+ !detail.Notes?.trim() &&
59
+ !detail.ProviderInstrumentRef?.trim() &&
60
+ !detail.ProviderCustomerRef?.trim() &&
61
+ !detail.StoredValueAccountID);
62
+ }
63
+ Validate() {
64
+ const result = super.Validate();
65
+ this.dropSavePopulatedFieldErrors(result);
66
+ return result;
67
+ }
68
+ dropSavePopulatedFieldErrors(result) {
69
+ const kept = result.Errors.filter((error) => !IsSavePopulatedFieldError(error.Source ?? '', this.IsSaved, () => false));
70
+ if (kept.length === result.Errors.length)
71
+ return;
72
+ result.Errors = kept;
73
+ result.Success = kept.every((error) => error.Type !== ValidationErrorType.Failure);
74
+ }
75
+ };
76
+ PaymentHeaderEntity = __decorate([
77
+ RegisterClass(BaseEntity, ENTITY)
78
+ ], PaymentHeaderEntity);
79
+ export { PaymentHeaderEntity };
80
+ //# sourceMappingURL=PaymentHeaderEntity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentHeaderEntity.js","sourceRoot":"","sources":["../src/PaymentHeaderEntity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Which pricing driver applies — and therefore whether a client may price locally.
3
+ *
4
+ * ## The question this answers
5
+ *
6
+ * Most pricing is metadata: price rules, tiers, lists, promotions, charge types, tax tables. A
7
+ * browser can run that walk itself against cached catalog data and show a total instantly, with no
8
+ * server round trip. What it cannot do is run a PLUGIN — a `BasePriceResolver` subclass, which is
9
+ * server-side code that may read anything and compute anything.
10
+ *
11
+ * So before pricing locally, a client has to know whether a plugin applies. That has to be
12
+ * answerable from METADATA, because asking the server defeats the point.
13
+ *
14
+ * ## Why the answer walks four levels
15
+ *
16
+ * The same shape `GLAccountResolver` already uses for accounts: the most specific answer wins.
17
+ *
18
+ * ```text
19
+ * Product.PricingDriverClass
20
+ * ↳ ProductCategory.PricingDriverClass (and up the parent chain)
21
+ * ↳ ProductType.PricingDriverClass
22
+ * ↳ OrderCompanyPolicy.PricingDriverClass
23
+ * ```
24
+ *
25
+ * A product may price specially on its own; a category may (event tickets); a type may (usage
26
+ * metering); a company may have a house resolver — which is where every plugin registered before the
27
+ * column existed is keyed, as `Company:<id>`.
28
+ *
29
+ * ## The direction of the safe answer
30
+ *
31
+ * `null` at every level means "no plugin", so price locally. **Anything else escalates.** When the
32
+ * data cannot be read at all this returns an ESCALATION rather than a local verdict: a failed lookup
33
+ * must not be mistaken for "no plugin found", because the two produce the same absence and only one
34
+ * of them is safe. Escalating costs a round trip; guessing costs a wrong price.
35
+ *
36
+ * @module @mj-biz-apps/orders-entities
37
+ */
38
+ import { type IRunViewProvider, type UserInfo } from '@memberjunction/core';
39
+ /** Where a driver was found, so a caller can say WHY it escalated. */
40
+ export type PricingDriverLevel = 'product' | 'category' | 'type' | 'company';
41
+ export interface PricingDriverVerdict {
42
+ /** True when every level is null and the caller may run the metadata walk itself. */
43
+ CanPriceLocally: boolean;
44
+ /** The ClassFactory key to invoke, when one applies. */
45
+ DriverClass: string | null;
46
+ /** Which level supplied it — for diagnostics, and for explaining an escalation. */
47
+ Level: PricingDriverLevel | null;
48
+ /**
49
+ * Set when the verdict is an escalation caused by a FAILED LOOKUP rather than a found driver.
50
+ * Never silently treated as "no driver" — see the module note on the direction of the safe answer.
51
+ */
52
+ Unresolved?: string;
53
+ }
54
+ /**
55
+ * The driver for one product, or a verdict that it can be priced locally.
56
+ *
57
+ * @param productID - The product being priced.
58
+ * @param companyID - The selling company, for the last fallback level.
59
+ */
60
+ export declare function ResolvePricingDriver(productID: string, companyID: string | null, provider: IRunViewProvider, user?: UserInfo): Promise<PricingDriverVerdict>;
61
+ /**
62
+ * True when EVERY product on an order can be priced locally.
63
+ *
64
+ * All-or-nothing on purpose. Pricing is not per-line arithmetic — promotions stack against order
65
+ * totals, charges apportion across lines, tax computes on the discounted amount — so an order with
66
+ * one plugin-priced line cannot be half-computed locally and half on the server without the two
67
+ * halves disagreeing about the same totals.
68
+ */
69
+ export declare function CanPriceOrderLocally(productIDs: string[], companyID: string | null, provider: IRunViewProvider, user?: UserInfo): Promise<PricingDriverVerdict>;
70
+ //# sourceMappingURL=PricingDriverResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PricingDriverResolver.d.ts","sourceRoot":"","sources":["../src/PricingDriverResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAW,KAAK,gBAAgB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErF,sEAAsE;AACtE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,oBAAoB;IACjC,qFAAqF;IACrF,eAAe,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,mFAAmF;IACnF,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAwBD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACtC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,CAAC,EAAE,QAAQ,GAChB,OAAO,CAAC,oBAAoB,CAAC,CA0F/B;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACtC,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,CAAC,EAAE,QAAQ,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAM/B"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @fileoverview Which pricing driver applies — and therefore whether a client may price locally.
3
+ *
4
+ * ## The question this answers
5
+ *
6
+ * Most pricing is metadata: price rules, tiers, lists, promotions, charge types, tax tables. A
7
+ * browser can run that walk itself against cached catalog data and show a total instantly, with no
8
+ * server round trip. What it cannot do is run a PLUGIN — a `BasePriceResolver` subclass, which is
9
+ * server-side code that may read anything and compute anything.
10
+ *
11
+ * So before pricing locally, a client has to know whether a plugin applies. That has to be
12
+ * answerable from METADATA, because asking the server defeats the point.
13
+ *
14
+ * ## Why the answer walks four levels
15
+ *
16
+ * The same shape `GLAccountResolver` already uses for accounts: the most specific answer wins.
17
+ *
18
+ * ```text
19
+ * Product.PricingDriverClass
20
+ * ↳ ProductCategory.PricingDriverClass (and up the parent chain)
21
+ * ↳ ProductType.PricingDriverClass
22
+ * ↳ OrderCompanyPolicy.PricingDriverClass
23
+ * ```
24
+ *
25
+ * A product may price specially on its own; a category may (event tickets); a type may (usage
26
+ * metering); a company may have a house resolver — which is where every plugin registered before the
27
+ * column existed is keyed, as `Company:<id>`.
28
+ *
29
+ * ## The direction of the safe answer
30
+ *
31
+ * `null` at every level means "no plugin", so price locally. **Anything else escalates.** When the
32
+ * data cannot be read at all this returns an ESCALATION rather than a local verdict: a failed lookup
33
+ * must not be mistaken for "no plugin found", because the two produce the same absence and only one
34
+ * of them is safe. Escalating costs a round trip; guessing costs a wrong price.
35
+ *
36
+ * @module @mj-biz-apps/orders-entities
37
+ */
38
+ import { RunView } from '@memberjunction/core';
39
+ /** Price locally — nothing anywhere names a plugin. */
40
+ const LOCAL = { CanPriceLocally: false, DriverClass: null, Level: null };
41
+ const PRODUCT_ENTITY = 'MJ_BizApps_Orders: Products';
42
+ const CATEGORY_ENTITY = 'MJ_BizApps_Orders: Product Categories';
43
+ const TYPE_ENTITY = 'MJ_BizApps_Orders: Product Types';
44
+ const POLICY_ENTITY = 'MJ_BizApps_Orders: Order Company Policies';
45
+ const UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
46
+ /**
47
+ * The driver for one product, or a verdict that it can be priced locally.
48
+ *
49
+ * @param productID - The product being priced.
50
+ * @param companyID - The selling company, for the last fallback level.
51
+ */
52
+ export async function ResolvePricingDriver(productID, companyID, provider, user) {
53
+ if (!UUID.test(productID ?? '')) {
54
+ return { ...LOCAL, CanPriceLocally: false, Unresolved: `'${productID}' is not a product id.` };
55
+ }
56
+ const rv = new RunView(provider);
57
+ const productRes = await rv.RunView({
58
+ EntityName: PRODUCT_ENTITY,
59
+ ExtraFilter: `ID = '${productID}'`,
60
+ Fields: ['ID', 'ProductTypeID', 'ProductCategoryID', 'PricingDriverClass'],
61
+ ResultType: 'simple',
62
+ }, user);
63
+ if (!productRes?.Success) {
64
+ return { ...LOCAL, Unresolved: `Could not read the product: ${productRes?.ErrorMessage ?? 'unknown error'}` };
65
+ }
66
+ const product = productRes.Results?.[0];
67
+ if (!product) {
68
+ return { ...LOCAL, Unresolved: `Product ${productID} does not exist.` };
69
+ }
70
+ if (product.PricingDriverClass) {
71
+ return { CanPriceLocally: false, DriverClass: product.PricingDriverClass, Level: 'product' };
72
+ }
73
+ // UP THE CATEGORY CHAIN. Walked one level at a time rather than read as a tree, because the
74
+ // nearest ancestor that names a driver wins and a deeper one must not override it.
75
+ let categoryID = product.ProductCategoryID;
76
+ const seen = new Set();
77
+ while (categoryID && UUID.test(categoryID)) {
78
+ if (seen.has(categoryID.toLowerCase()))
79
+ break; // a cycle is bad data, not an infinite loop
80
+ seen.add(categoryID.toLowerCase());
81
+ const catRes = await rv.RunView({
82
+ EntityName: CATEGORY_ENTITY,
83
+ ExtraFilter: `ID = '${categoryID}'`,
84
+ Fields: ['ID', 'ParentProductCategoryID', 'PricingDriverClass'],
85
+ ResultType: 'simple',
86
+ }, user);
87
+ if (!catRes?.Success) {
88
+ return { ...LOCAL, Unresolved: `Could not read product category ${categoryID}.` };
89
+ }
90
+ const category = catRes.Results?.[0];
91
+ if (!category)
92
+ break;
93
+ if (category.PricingDriverClass) {
94
+ return { CanPriceLocally: false, DriverClass: category.PricingDriverClass, Level: 'category' };
95
+ }
96
+ categoryID = category.ParentProductCategoryID;
97
+ }
98
+ if (product.ProductTypeID && UUID.test(product.ProductTypeID)) {
99
+ const typeRes = await rv.RunView({
100
+ EntityName: TYPE_ENTITY,
101
+ ExtraFilter: `ID = '${product.ProductTypeID}'`,
102
+ Fields: ['PricingDriverClass'],
103
+ ResultType: 'simple',
104
+ }, user);
105
+ if (!typeRes?.Success) {
106
+ return { ...LOCAL, Unresolved: `Could not read product type ${product.ProductTypeID}.` };
107
+ }
108
+ const driver = typeRes.Results?.[0]?.PricingDriverClass;
109
+ if (driver)
110
+ return { CanPriceLocally: false, DriverClass: driver, Level: 'type' };
111
+ }
112
+ if (companyID && UUID.test(companyID)) {
113
+ const policyRes = await rv.RunView({
114
+ EntityName: POLICY_ENTITY,
115
+ ExtraFilter: `ID = '${companyID}'`,
116
+ Fields: ['PricingDriverClass'],
117
+ ResultType: 'simple',
118
+ }, user);
119
+ if (!policyRes?.Success) {
120
+ return { ...LOCAL, Unresolved: `Could not read the company pricing policy.` };
121
+ }
122
+ const driver = policyRes.Results?.[0]?.PricingDriverClass;
123
+ if (driver)
124
+ return { CanPriceLocally: false, DriverClass: driver, Level: 'company' };
125
+ }
126
+ // Nothing anywhere names a plugin: the metadata walk is the whole answer, so a client may run it.
127
+ return { CanPriceLocally: true, DriverClass: null, Level: null };
128
+ }
129
+ /**
130
+ * True when EVERY product on an order can be priced locally.
131
+ *
132
+ * All-or-nothing on purpose. Pricing is not per-line arithmetic — promotions stack against order
133
+ * totals, charges apportion across lines, tax computes on the discounted amount — so an order with
134
+ * one plugin-priced line cannot be half-computed locally and half on the server without the two
135
+ * halves disagreeing about the same totals.
136
+ */
137
+ export async function CanPriceOrderLocally(productIDs, companyID, provider, user) {
138
+ for (const productID of [...new Set(productIDs)]) {
139
+ const verdict = await ResolvePricingDriver(productID, companyID, provider, user);
140
+ if (!verdict.CanPriceLocally)
141
+ return verdict;
142
+ }
143
+ return { CanPriceLocally: true, DriverClass: null, Level: null };
144
+ }
145
+ //# sourceMappingURL=PricingDriverResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PricingDriverResolver.js","sourceRoot":"","sources":["../src/PricingDriverResolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview `PromotionCodesCompanion` — the codes a customer presented, riding with the order.
3
+ *
4
+ * ## Why this is a companion and not a related-record collection
5
+ *
6
+ * Charges and manual discounts became collections because they have child ROWS with an
7
+ * `OrderHeaderID` — a client can stage one and the engine completes it. A promotion code has no such
8
+ * row. `PromotionCode` is catalog metadata (which codes exist); the RESULT of applying one is an
9
+ * `OrderAdjustment` with `PromotionCodeID` set, and only the engine can produce that.
10
+ *
11
+ * At compose time the browser holds a **string**. It does not know the code's id, whether this
12
+ * customer qualifies, what the promotion is worth against these lines, whether it stacks with
13
+ * another, or whether the promotion has hit its redemption cap — that last one depending on orders
14
+ * other people placed a second ago. Forcing it into a collection would mean inventing a table to
15
+ * hold a request, which is the mirror problem wearing a different hat.
16
+ *
17
+ * So it is what MJ's companion abstraction is actually for: state that belongs to the record, is not
18
+ * one of its fields, and has to cross the wire.
19
+ *
20
+ * ## What this fixes
21
+ *
22
+ * `OrderEntityServer._promotionCodes` was a transient array only the server could populate. While
23
+ * `OrderDraft` existed its hydrator carried the codes across; when the draft was deleted nothing
24
+ * replaced that path, so a code typed on screen was priced into the PREVIEW — `Orders.PriceOrder`
25
+ * receives them directly — and then silently dropped at confirm. The customer saw a discount and was
26
+ * billed full price, with every total on the order agreeing with every other total.
27
+ *
28
+ * ## Contributes nothing to the save plan, deliberately
29
+ *
30
+ * A code is an input. It produces no row of its own, so there is nothing for it to write; the
31
+ * adjustment rows the engine derives from it are written by the promotion engine, inside the booking
32
+ * transaction, where the decision is actually made.
33
+ *
34
+ * @module @mj-biz-apps/orders-entities
35
+ */
36
+ import { EntityCompanion, type EntityCompanionDeserializeMode } from '@memberjunction/core';
37
+ /** The wire shape: just the codes, in the order the customer gave them. */
38
+ export type PromotionCodesWire = string[];
39
+ export declare class PromotionCodesCompanion extends EntityCompanion<PromotionCodesWire> {
40
+ /**
41
+ * The wire key. **Published contract** — renaming it silently drops codes from in-flight
42
+ * payloads written by the other side of a rolling deploy, and a dropped code is a discount the
43
+ * customer was promised and did not get.
44
+ */
45
+ readonly Name = "PromotionCodes";
46
+ private codes;
47
+ /** The codes presented, de-duplicated and trimmed. */
48
+ get Codes(): string[];
49
+ set Codes(value: string[]);
50
+ /** Present a code. Adding one twice is not two discounts, so it is not two entries. */
51
+ Add(code: string): void;
52
+ /** Withdraw a code the customer removed before saving. */
53
+ Remove(code: string): void;
54
+ Clear(): void;
55
+ /**
56
+ * `null` when there are no codes, so an ordinary save does not pay to ship an empty array.
57
+ */
58
+ Serialize(): Promise<PromotionCodesWire | null>;
59
+ /**
60
+ * Restores the codes from the other tier.
61
+ *
62
+ * `mode` is ignored on purpose. A code list is a REQUEST in both directions: there is no
63
+ * server-authoritative version of "what the customer typed" to overwrite the client's copy with,
64
+ * and the engine's verdict on each code travels back separately as `UnusablePromotionCodes` —
65
+ * which says WHY a code did nothing, and is the thing worth showing.
66
+ */
67
+ Deserialize(data: PromotionCodesWire, _mode: EntityCompanionDeserializeMode): Promise<void>;
68
+ /**
69
+ * Dirty when any code is present.
70
+ *
71
+ * Load-bearing rather than tidy: an order whose ONLY change is a promotion code must still save.
72
+ * Reporting clean here would return early from `Save()` and drop the code — the same silent loss
73
+ * this companion exists to end, reintroduced one level down.
74
+ */
75
+ get Dirty(): boolean;
76
+ }
77
+ //# sourceMappingURL=PromotionCodesCompanion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PromotionCodesCompanion.d.ts","sourceRoot":"","sources":["../src/PromotionCodesCompanion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,eAAe,EAAE,KAAK,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAE5F,2EAA2E;AAC3E,MAAM,MAAM,kBAAkB,GAAG,MAAM,EAAE,CAAC;AAE1C,qBAAa,uBAAwB,SAAQ,eAAe,CAAC,kBAAkB,CAAC;IAC5E;;;;OAIG;IACH,SAAgB,IAAI,oBAAoB;IAExC,OAAO,CAAC,KAAK,CAAgB;IAE7B,sDAAsD;IACtD,IAAW,KAAK,IAAI,MAAM,EAAE,CAE3B;IAED,IAAW,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAE/B;IAED,uFAAuF;IAChF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,0DAA0D;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK1B,KAAK,IAAI,IAAI;IAIpB;;OAEG;IACU,SAAS,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAI5D;;;;;;;OAOG;IACU,WAAW,CAAC,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,8BAA8B,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxG;;;;;;OAMG;IACH,IAAoB,KAAK,IAAI,OAAO,CAEnC;CACJ"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @fileoverview `PromotionCodesCompanion` — the codes a customer presented, riding with the order.
3
+ *
4
+ * ## Why this is a companion and not a related-record collection
5
+ *
6
+ * Charges and manual discounts became collections because they have child ROWS with an
7
+ * `OrderHeaderID` — a client can stage one and the engine completes it. A promotion code has no such
8
+ * row. `PromotionCode` is catalog metadata (which codes exist); the RESULT of applying one is an
9
+ * `OrderAdjustment` with `PromotionCodeID` set, and only the engine can produce that.
10
+ *
11
+ * At compose time the browser holds a **string**. It does not know the code's id, whether this
12
+ * customer qualifies, what the promotion is worth against these lines, whether it stacks with
13
+ * another, or whether the promotion has hit its redemption cap — that last one depending on orders
14
+ * other people placed a second ago. Forcing it into a collection would mean inventing a table to
15
+ * hold a request, which is the mirror problem wearing a different hat.
16
+ *
17
+ * So it is what MJ's companion abstraction is actually for: state that belongs to the record, is not
18
+ * one of its fields, and has to cross the wire.
19
+ *
20
+ * ## What this fixes
21
+ *
22
+ * `OrderEntityServer._promotionCodes` was a transient array only the server could populate. While
23
+ * `OrderDraft` existed its hydrator carried the codes across; when the draft was deleted nothing
24
+ * replaced that path, so a code typed on screen was priced into the PREVIEW — `Orders.PriceOrder`
25
+ * receives them directly — and then silently dropped at confirm. The customer saw a discount and was
26
+ * billed full price, with every total on the order agreeing with every other total.
27
+ *
28
+ * ## Contributes nothing to the save plan, deliberately
29
+ *
30
+ * A code is an input. It produces no row of its own, so there is nothing for it to write; the
31
+ * adjustment rows the engine derives from it are written by the promotion engine, inside the booking
32
+ * transaction, where the decision is actually made.
33
+ *
34
+ * @module @mj-biz-apps/orders-entities
35
+ */
36
+ import { EntityCompanion } from '@memberjunction/core';
37
+ export class PromotionCodesCompanion extends EntityCompanion {
38
+ constructor() {
39
+ super(...arguments);
40
+ /**
41
+ * The wire key. **Published contract** — renaming it silently drops codes from in-flight
42
+ * payloads written by the other side of a rolling deploy, and a dropped code is a discount the
43
+ * customer was promised and did not get.
44
+ */
45
+ this.Name = 'PromotionCodes';
46
+ this.codes = [];
47
+ }
48
+ /** The codes presented, de-duplicated and trimmed. */
49
+ get Codes() {
50
+ return [...this.codes];
51
+ }
52
+ set Codes(value) {
53
+ this.codes = normalize(value);
54
+ }
55
+ /** Present a code. Adding one twice is not two discounts, so it is not two entries. */
56
+ Add(code) {
57
+ this.codes = normalize([...this.codes, code]);
58
+ }
59
+ /** Withdraw a code the customer removed before saving. */
60
+ Remove(code) {
61
+ const key = code.trim().toLowerCase();
62
+ this.codes = this.codes.filter((c) => c.toLowerCase() !== key);
63
+ }
64
+ Clear() {
65
+ this.codes = [];
66
+ }
67
+ /**
68
+ * `null` when there are no codes, so an ordinary save does not pay to ship an empty array.
69
+ */
70
+ async Serialize() {
71
+ return this.codes.length ? [...this.codes] : null;
72
+ }
73
+ /**
74
+ * Restores the codes from the other tier.
75
+ *
76
+ * `mode` is ignored on purpose. A code list is a REQUEST in both directions: there is no
77
+ * server-authoritative version of "what the customer typed" to overwrite the client's copy with,
78
+ * and the engine's verdict on each code travels back separately as `UnusablePromotionCodes` —
79
+ * which says WHY a code did nothing, and is the thing worth showing.
80
+ */
81
+ async Deserialize(data, _mode) {
82
+ this.codes = normalize(Array.isArray(data) ? data : []);
83
+ }
84
+ /**
85
+ * Dirty when any code is present.
86
+ *
87
+ * Load-bearing rather than tidy: an order whose ONLY change is a promotion code must still save.
88
+ * Reporting clean here would return early from `Save()` and drop the code — the same silent loss
89
+ * this companion exists to end, reintroduced one level down.
90
+ */
91
+ get Dirty() {
92
+ return this.codes.length > 0;
93
+ }
94
+ }
95
+ /** Trimmed, non-empty, case-insensitively de-duplicated, first occurrence wins. */
96
+ function normalize(values) {
97
+ const seen = new Set();
98
+ const out = [];
99
+ for (const raw of values ?? []) {
100
+ const code = String(raw ?? '').trim();
101
+ if (!code)
102
+ continue;
103
+ const key = code.toLowerCase();
104
+ if (seen.has(key))
105
+ continue;
106
+ seen.add(key);
107
+ out.push(code);
108
+ }
109
+ return out;
110
+ }
111
+ //# sourceMappingURL=PromotionCodesCompanion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PromotionCodesCompanion.js","sourceRoot":"","sources":["../src/PromotionCodesCompanion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,eAAe,EAAuC,MAAM,sBAAsB,CAAC;AAK5F,MAAM,OAAO,uBAAwB,SAAQ,eAAmC;IAAhF;;QACI;;;;WAIG;QACa,SAAI,GAAG,gBAAgB,CAAC;QAEhC,UAAK,GAAa,EAAE,CAAC;IAuDjC,CAAC;IArDG,sDAAsD;IACtD,IAAW,KAAK;QACZ,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,IAAW,KAAK,CAAC,KAAe;QAC5B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,uFAAuF;IAChF,GAAG,CAAC,IAAY;QACnB,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,0DAA0D;IACnD,MAAM,CAAC,IAAY;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,CAAC;IACnE,CAAC;IAEM,KAAK;QACR,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IACpB,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,SAAS;QAClB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,WAAW,CAAC,IAAwB,EAAE,KAAqC;QACpF,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;;OAMG;IACH,IAAoB,KAAK;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IACjC,CAAC;CACJ;AAED,mFAAmF;AACnF,SAAS,SAAS,CAAC,MAAmC;IAClD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * What a booked order may not change — stated once, enforced from Validate().
3
+ *
4
+ * Confirm books journal entries. A later edit that adds a line, reprices one, or
5
+ * restates the initial tender would leave the ledger pointing at a different
6
+ * amount than the order. Trigger 51003 freezes money columns on an *existing*
7
+ * line update; it does not stop inserting a new line onto a confirmed order.
8
+ *
9
+ * @module @mj-biz-apps/orders-entities
10
+ */
11
+ /** Line columns that change what was booked. */
12
+ export declare const ORDER_LINE_MONEY_FIELDS: readonly ["Quantity", "UnitPrice", "DiscountPct", "DiscountAmount", "LineTax", "ChargeAmount", "LineTotalNet", "LineTotalGross"];
13
+ /** Header columns that change what was booked. */
14
+ export declare const ORDER_HEADER_MONEY_FIELDS: readonly ["InitialPaymentTypeID", "InitialPaymentAmount", "InitialPaymentDetailID", "CompanyID"];
15
+ export interface BookedMoneyEditFacts {
16
+ NewLineCount: number;
17
+ RemovedLineCount: number;
18
+ DirtyLineMoneyFields: string[];
19
+ ChargesChanged: boolean;
20
+ AdjustmentsChanged: boolean;
21
+ DirtyHeaderMoneyFields: string[];
22
+ }
23
+ /**
24
+ * Human-readable refusal, or null when this save does not touch booked money.
25
+ */
26
+ export declare function BookedMoneyEditMessage(facts: BookedMoneyEditFacts): string | null;
27
+ //# sourceMappingURL=booked-money.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booked-money.d.ts","sourceRoot":"","sources":["../src/booked-money.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,kIAS1B,CAAC;AAEX,kDAAkD;AAClD,eAAO,MAAM,yBAAyB,kGAK5B,CAAC;AAEX,MAAM,WAAW,oBAAoB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,MAAM,EAAE,CAAC;CACpC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,GAAG,IAAI,CA6BjF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * What a booked order may not change — stated once, enforced from Validate().
3
+ *
4
+ * Confirm books journal entries. A later edit that adds a line, reprices one, or
5
+ * restates the initial tender would leave the ledger pointing at a different
6
+ * amount than the order. Trigger 51003 freezes money columns on an *existing*
7
+ * line update; it does not stop inserting a new line onto a confirmed order.
8
+ *
9
+ * @module @mj-biz-apps/orders-entities
10
+ */
11
+ /** Line columns that change what was booked. */
12
+ export const ORDER_LINE_MONEY_FIELDS = [
13
+ 'Quantity',
14
+ 'UnitPrice',
15
+ 'DiscountPct',
16
+ 'DiscountAmount',
17
+ 'LineTax',
18
+ 'ChargeAmount',
19
+ 'LineTotalNet',
20
+ 'LineTotalGross',
21
+ ];
22
+ /** Header columns that change what was booked. */
23
+ export const ORDER_HEADER_MONEY_FIELDS = [
24
+ 'InitialPaymentTypeID',
25
+ 'InitialPaymentAmount',
26
+ 'InitialPaymentDetailID',
27
+ 'CompanyID',
28
+ ];
29
+ /**
30
+ * Human-readable refusal, or null when this save does not touch booked money.
31
+ */
32
+ export function BookedMoneyEditMessage(facts) {
33
+ const parts = [];
34
+ if (facts.NewLineCount > 0) {
35
+ parts.push(facts.NewLineCount === 1 ? 'add a line' : `add ${facts.NewLineCount} lines`);
36
+ }
37
+ if (facts.RemovedLineCount > 0) {
38
+ parts.push(facts.RemovedLineCount === 1 ? 'remove a line' : `remove ${facts.RemovedLineCount} lines`);
39
+ }
40
+ if (facts.DirtyLineMoneyFields.length > 0) {
41
+ parts.push(`change ${uniqueJoin(facts.DirtyLineMoneyFields)}`);
42
+ }
43
+ if (facts.ChargesChanged) {
44
+ parts.push('change charges');
45
+ }
46
+ if (facts.AdjustmentsChanged) {
47
+ parts.push('change adjustments');
48
+ }
49
+ if (facts.DirtyHeaderMoneyFields.length > 0) {
50
+ parts.push(`change ${uniqueJoin(facts.DirtyHeaderMoneyFields)}`);
51
+ }
52
+ if (parts.length === 0)
53
+ return null;
54
+ return (`This order is booked — it cannot ${joinList(parts)}. ` +
55
+ `Voiding (and a reversal order) is how booked money is undone, not an edit of the original.`);
56
+ }
57
+ function uniqueJoin(names) {
58
+ return joinList([...new Set(names)]);
59
+ }
60
+ function joinList(parts) {
61
+ if (parts.length === 1)
62
+ return parts[0];
63
+ if (parts.length === 2)
64
+ return `${parts[0]} or ${parts[1]}`;
65
+ return `${parts.slice(0, -1).join(', ')}, or ${parts[parts.length - 1]}`;
66
+ }
67
+ //# sourceMappingURL=booked-money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booked-money.js","sourceRoot":"","sources":["../src/booked-money.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gDAAgD;AAChD,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACnC,UAAU;IACV,WAAW;IACX,aAAa;IACb,gBAAgB;IAChB,SAAS;IACT,cAAc;IACd,cAAc;IACd,gBAAgB;CACV,CAAC;AAEX,kDAAkD;AAClD,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACrC,sBAAsB;IACtB,sBAAsB;IACtB,wBAAwB;IACxB,WAAW;CACL,CAAC;AAWX;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAA2B;IAC9D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CACN,KAAK,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,YAAY,QAAQ,CAC9E,CAAC;IACN,CAAC;IACD,IAAI,KAAK,CAAC,gBAAgB,GAAG,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CACN,KAAK,CAAC,gBAAgB,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,gBAAgB,QAAQ,CAC5F,CAAC;IACN,CAAC;IACD,IAAI,KAAK,CAAC,oBAAoB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,UAAU,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,KAAK,CAAC,kBAAkB,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,KAAK,CAAC,sBAAsB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,UAAU,UAAU,CAAC,KAAK,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,CACH,oCAAoC,QAAQ,CAAC,KAAK,CAAC,IAAI;QACvD,4FAA4F,CAC/F,CAAC;AACN,CAAC;AAED,SAAS,UAAU,CAAC,KAAe;IAC/B,OAAO,QAAQ,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,QAAQ,CAAC,KAAe;IAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;AAC7E,CAAC"}