@dainamite/cpq 0.2.2 → 0.4.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.
- package/CHANGELOG.md +62 -0
- package/MIGRATION.md +34 -1
- package/README.md +17 -12
- package/dist/modules/cpq/api/product-attributes/route.js +28 -53
- package/dist/modules/cpq/api/product-attributes/route.js.map +2 -2
- package/dist/modules/cpq/api/product-charges/route.js +12 -2
- package/dist/modules/cpq/api/product-charges/route.js.map +2 -2
- package/dist/modules/cpq/api/product-offerings/route.js +2 -0
- package/dist/modules/cpq/api/product-offerings/route.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +128 -59
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +11 -11
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +55 -1
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
- package/dist/modules/cpq/cli.js +58 -4
- package/dist/modules/cpq/cli.js.map +3 -3
- package/dist/modules/cpq/data/charge-pricing.js +12 -0
- package/dist/modules/cpq/data/charge-pricing.js.map +7 -0
- package/dist/modules/cpq/data/entities.js +3 -0
- package/dist/modules/cpq/data/entities.js.map +2 -2
- package/dist/modules/cpq/data/validators.js +36 -15
- package/dist/modules/cpq/data/validators.js.map +2 -2
- package/dist/modules/cpq/events.js +7 -0
- package/dist/modules/cpq/events.js.map +2 -2
- package/dist/modules/cpq/lib/seeds/demo.js +103 -0
- package/dist/modules/cpq/lib/seeds/demo.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.js +28 -0
- package/dist/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.js.map +7 -0
- package/dist/modules/cpq/services/cpqOrderService.js +130 -76
- package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
- package/dist/modules/cpq/services/cpqPricingService.js +53 -29
- package/dist/modules/cpq/services/cpqPricingService.js.map +3 -3
- package/dist/modules/cpq/services/types.js.map +2 -2
- package/package.json +11 -9
- package/src/modules/cpq/__integration__/TC-CPQ-012.spec.ts +34 -4
- package/src/modules/cpq/__tests__/package-isolation.test.ts +8 -3
- package/src/modules/cpq/api/product-attributes/__tests__/serializeAttribute.test.ts +62 -0
- package/src/modules/cpq/api/product-attributes/route.ts +34 -52
- package/src/modules/cpq/api/product-charges/route.ts +23 -2
- package/src/modules/cpq/api/product-offerings/route.ts +2 -0
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +151 -62
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -22
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +73 -1
- package/src/modules/cpq/cli.ts +83 -4
- package/src/modules/cpq/data/__tests__/charge-pricing.test.ts +49 -0
- package/src/modules/cpq/data/__tests__/validators.test.ts +60 -5
- package/src/modules/cpq/data/charge-pricing.ts +48 -0
- package/src/modules/cpq/data/entities.ts +10 -0
- package/src/modules/cpq/data/validators.ts +56 -26
- package/src/modules/cpq/events.ts +11 -3
- package/src/modules/cpq/lib/seeds/__tests__/demo.test.ts +78 -0
- package/src/modules/cpq/lib/seeds/demo.ts +145 -0
- package/src/modules/cpq/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.ts +57 -0
- package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +172 -1
- package/src/modules/cpq/services/cpqOrderService.ts +214 -103
- package/src/modules/cpq/services/cpqPricingService.ts +82 -34
- package/src/modules/cpq/services/types.ts +4 -1
package/dist/modules/cpq/cli.js
CHANGED
|
@@ -1,10 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { seedCpqDemo } from "./lib/seeds/demo.js";
|
|
3
|
+
function parseArgs(rest) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (let i = 0; i < rest.length; i++) {
|
|
6
|
+
const token = rest[i];
|
|
7
|
+
if (!token || !token.startsWith("--")) continue;
|
|
8
|
+
const key = token.slice(2);
|
|
9
|
+
const next = rest[i + 1];
|
|
10
|
+
if (next === void 0 || next.startsWith("--")) out[key] = true;
|
|
11
|
+
else {
|
|
12
|
+
out[key] = next;
|
|
13
|
+
i++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
async function resolveScope(em, args) {
|
|
19
|
+
const orgArg = typeof args.org === "string" ? args.org : void 0;
|
|
20
|
+
const tenantArg = typeof args.tenant === "string" ? args.tenant : void 0;
|
|
21
|
+
const { Organization } = await import("@open-mercato/core/modules/directory/data/entities");
|
|
22
|
+
if (orgArg) {
|
|
23
|
+
const org2 = await em.findOne(Organization, { id: orgArg });
|
|
24
|
+
if (!org2) return null;
|
|
25
|
+
return { organizationId: org2.id, tenantId: tenantArg ?? org2.tenant.id };
|
|
26
|
+
}
|
|
27
|
+
const orgs = await em.find(
|
|
28
|
+
Organization,
|
|
29
|
+
{ isActive: true },
|
|
30
|
+
{ orderBy: { createdAt: "asc" }, limit: 5 }
|
|
31
|
+
);
|
|
32
|
+
if (orgs.length === 0) return null;
|
|
33
|
+
const org = orgs[0];
|
|
34
|
+
if (orgs.length > 1) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[cpq seed] Multiple organizations found \u2014 defaulting to "${org.name}" (${org.id}). Pass --org <id> to target another.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return { organizationId: org.id, tenantId: tenantArg ?? org.tenant.id };
|
|
40
|
+
}
|
|
41
|
+
const seed = {
|
|
2
42
|
command: "seed",
|
|
3
|
-
async run() {
|
|
4
|
-
|
|
43
|
+
async run(rest) {
|
|
44
|
+
const args = parseArgs(rest);
|
|
45
|
+
const reset = Boolean(args.reset);
|
|
46
|
+
const container = await createRequestContainer();
|
|
47
|
+
const em = container.resolve("em");
|
|
48
|
+
const scope = await resolveScope(em, args);
|
|
49
|
+
if (!scope) {
|
|
50
|
+
console.error('[cpq seed] No organization found. Run "yarn initialize" first, or pass --org <id>.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const res = await seedCpqDemo(em, scope, { reset });
|
|
54
|
+
console.log(
|
|
55
|
+
`[cpq seed] tenant=${scope.tenantId} org=${scope.organizationId}${reset ? " (reset)" : ""}
|
|
56
|
+
\u2713 products +${res.productsCreated}, specs +${res.specsCreated}, offerings +${res.offeringsCreated}, charges +${res.chargesCreated} (existing rows left untouched)
|
|
57
|
+
\u2192 Backend \xB7 CPQ \xB7 Product Specifications / Product Offerings.`
|
|
58
|
+
);
|
|
5
59
|
}
|
|
6
60
|
};
|
|
7
|
-
var cli_default = [
|
|
61
|
+
var cli_default = [seed];
|
|
8
62
|
export {
|
|
9
63
|
cli_default as default
|
|
10
64
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/cpq/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\n\nconst
|
|
5
|
-
"mappings": "AAEA,MAAM,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { seedCpqDemo } from './lib/seeds/demo'\n\n/**\n * `mercato cpq seed [--reset] [--tenant <id>] [--org <id>]`\n *\n * Loads a minimal, neutral CPQ test catalog (product + specification +\n * offering + charge) into a single tenant. With no `--tenant`/`--org` it\n * resolves the first (primary) organization created by `mercato init`.\n * `--reset` clears the seed's own rows first.\n *\n * For rich, vertical demo catalogs use `mercato demo_tenants seed --all`.\n */\n\nfunction parseArgs(rest: string[]): Record<string, string | boolean> {\n const out: Record<string, string | boolean> = {}\n for (let i = 0; i < rest.length; i++) {\n const token = rest[i]\n if (!token || !token.startsWith('--')) continue\n const key = token.slice(2)\n const next = rest[i + 1]\n if (next === undefined || next.startsWith('--')) out[key] = true\n else {\n out[key] = next\n i++\n }\n }\n return out\n}\n\nasync function resolveScope(\n em: EntityManager,\n args: Record<string, string | boolean>,\n): Promise<{ tenantId: string; organizationId: string } | null> {\n const orgArg = typeof args.org === 'string' ? args.org : undefined\n const tenantArg = typeof args.tenant === 'string' ? args.tenant : undefined\n\n const { Organization } = await import('@open-mercato/core/modules/directory/data/entities')\n\n if (orgArg) {\n const org = await em.findOne(Organization, { id: orgArg } as never)\n if (!org) return null\n return { organizationId: org.id, tenantId: tenantArg ?? org.tenant.id }\n }\n\n const orgs = await em.find(\n Organization,\n { isActive: true } as never,\n { orderBy: { createdAt: 'asc' } as never, limit: 5 },\n )\n if (orgs.length === 0) return null\n const org = orgs[0]\n if (orgs.length > 1) {\n console.warn(\n `[cpq seed] Multiple organizations found \u2014 defaulting to \"${org.name}\" (${org.id}). ` +\n `Pass --org <id> to target another.`,\n )\n }\n return { organizationId: org.id, tenantId: tenantArg ?? org.tenant.id }\n}\n\nconst seed: ModuleCli = {\n command: 'seed',\n async run(rest) {\n const args = parseArgs(rest)\n const reset = Boolean(args.reset)\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const scope = await resolveScope(em, args)\n if (!scope) {\n console.error('[cpq seed] No organization found. Run \"yarn initialize\" first, or pass --org <id>.')\n return\n }\n\n const res = await seedCpqDemo(em, scope, { reset })\n console.log(\n `[cpq seed] tenant=${scope.tenantId} org=${scope.organizationId}${reset ? ' (reset)' : ''}\\n` +\n ` \u2713 products +${res.productsCreated}, specs +${res.specsCreated}, ` +\n `offerings +${res.offeringsCreated}, charges +${res.chargesCreated} (existing rows left untouched)\\n` +\n ` \u2192 Backend \u00B7 CPQ \u00B7 Product Specifications / Product Offerings.`,\n )\n },\n}\n\nexport default [seed]\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,mBAAmB;AAa5B,SAAS,UAAU,MAAkD;AACnE,QAAM,MAAwC,CAAC;AAC/C,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,CAAC,SAAS,CAAC,MAAM,WAAW,IAAI,EAAG;AACvC,UAAM,MAAM,MAAM,MAAM,CAAC;AACzB,UAAM,OAAO,KAAK,IAAI,CAAC;AACvB,QAAI,SAAS,UAAa,KAAK,WAAW,IAAI,EAAG,KAAI,GAAG,IAAI;AAAA,SACvD;AACH,UAAI,GAAG,IAAI;AACX;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,aACb,IACA,MAC8D;AAC9D,QAAM,SAAS,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;AACzD,QAAM,YAAY,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;AAElE,QAAM,EAAE,aAAa,IAAI,MAAM,OAAO,oDAAoD;AAE1F,MAAI,QAAQ;AACV,UAAMA,OAAM,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,OAAO,CAAU;AAClE,QAAI,CAACA,KAAK,QAAO;AACjB,WAAO,EAAE,gBAAgBA,KAAI,IAAI,UAAU,aAAaA,KAAI,OAAO,GAAG;AAAA,EACxE;AAEA,QAAM,OAAO,MAAM,GAAG;AAAA,IACpB;AAAA,IACA,EAAE,UAAU,KAAK;AAAA,IACjB,EAAE,SAAS,EAAE,WAAW,MAAM,GAAY,OAAO,EAAE;AAAA,EACrD;AACA,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,MAAM,KAAK,CAAC;AAClB,MAAI,KAAK,SAAS,GAAG;AACnB,YAAQ;AAAA,MACN,iEAA4D,IAAI,IAAI,MAAM,IAAI,EAAE;AAAA,IAElF;AAAA,EACF;AACA,SAAO,EAAE,gBAAgB,IAAI,IAAI,UAAU,aAAa,IAAI,OAAO,GAAG;AACxE;AAEA,MAAM,OAAkB;AAAA,EACtB,SAAS;AAAA,EACT,MAAM,IAAI,MAAM;AACd,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,QAAQ,QAAQ,KAAK,KAAK;AAEhC,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAuB,IAAI;AAEhD,UAAM,QAAQ,MAAM,aAAa,IAAI,IAAI;AACzC,QAAI,CAAC,OAAO;AACV,cAAQ,MAAM,oFAAoF;AAClG;AAAA,IACF;AAEA,UAAM,MAAM,MAAM,YAAY,IAAI,OAAO,EAAE,MAAM,CAAC;AAClD,YAAQ;AAAA,MACN,qBAAqB,MAAM,QAAQ,QAAQ,MAAM,cAAc,GAAG,QAAQ,aAAa,EAAE;AAAA,qBACtE,IAAI,eAAe,YAAY,IAAI,YAAY,gBAClD,IAAI,gBAAgB,cAAc,IAAI,cAAc;AAAA;AAAA,IAEtE;AAAA,EACF;AACF;AAEA,IAAO,cAAQ,CAAC,IAAI;",
|
|
6
|
+
"names": ["org"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function normalizeChargePricing(charge) {
|
|
2
|
+
const pm = (charge.pricingMethod ?? "").toLowerCase();
|
|
3
|
+
const tableLike = pm === "table" || pm === "per_unit" || pm === "tiered" || pm === "volume";
|
|
4
|
+
const source = tableLike ? "table" : pm === "fixed" ? "fixed" : charge.pricingTableId ? "table" : "fixed";
|
|
5
|
+
const rawModel = (charge.chargeModel ?? "").toLowerCase();
|
|
6
|
+
const model = rawModel === "flat" || rawModel === "per_unit" || rawModel === "volume" || rawModel === "tiered" ? rawModel : pm === "per_unit" ? "per_unit" : pm === "tiered" ? "tiered" : "flat";
|
|
7
|
+
return { model, source };
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
normalizeChargePricing
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=charge-pricing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq/data/charge-pricing.ts"],
|
|
4
|
+
"sourcesContent": ["// XD-297: a charge's \"shape\" is two orthogonal axes:\n// ChargeModel \u2014 how quantity drives the charge.\n// PricingSource \u2014 where the unit price comes from.\n// Pure, dependency-free helpers live in the data layer so validators, routes,\n// the pricing service, seeds, and the UI can all share them without reaching\n// across layers.\nexport type ChargeModel = 'flat' | 'per_unit' | 'volume' | 'tiered'\nexport type PricingSource = 'fixed' | 'table'\n\n/**\n * Resolve a charge's (chargeModel, pricingSource) from either the new split\n * columns or a legacy combined `pricingMethod` value (flat | per_unit | tiered).\n * Keeps the engine working for rows that predate the split / haven't been\n * migrated, and tolerates new clients that send the legacy value by mistake.\n */\nexport function normalizeChargePricing(charge: {\n chargeModel?: string | null\n pricingMethod?: string | null\n fixedPrice?: string | null\n pricingTableId?: string | null\n}): { model: ChargeModel; source: PricingSource } {\n const pm = (charge.pricingMethod ?? '').toLowerCase()\n // Anything that historically meant \"comes from a table\" maps to source=table.\n const tableLike = pm === 'table' || pm === 'per_unit' || pm === 'tiered' || pm === 'volume'\n const source: PricingSource = tableLike\n ? 'table'\n : pm === 'fixed'\n ? 'fixed'\n // Legacy 'flat' could be a fixed price OR a flat table lookup (the old\n // `calculateFlat`-via-table path), empty, or unknown: infer from the\n // presence of a pricing table \u2014 preserving that legacy behaviour.\n : charge.pricingTableId\n ? 'table'\n : 'fixed'\n\n const rawModel = (charge.chargeModel ?? '').toLowerCase()\n const model: ChargeModel =\n rawModel === 'flat' || rawModel === 'per_unit' || rawModel === 'volume' || rawModel === 'tiered'\n ? (rawModel as ChargeModel)\n // No explicit model \u2192 derive from the legacy combined pricingMethod.\n : pm === 'per_unit'\n ? 'per_unit'\n : pm === 'tiered'\n ? 'tiered'\n : 'flat'\n\n return { model, source }\n}\n"],
|
|
5
|
+
"mappings": "AAeO,SAAS,uBAAuB,QAKW;AAChD,QAAM,MAAM,OAAO,iBAAiB,IAAI,YAAY;AAEpD,QAAM,YAAY,OAAO,WAAW,OAAO,cAAc,OAAO,YAAY,OAAO;AACnF,QAAM,SAAwB,YAC1B,UACA,OAAO,UACL,UAIA,OAAO,iBACL,UACA;AAER,QAAM,YAAY,OAAO,eAAe,IAAI,YAAY;AACxD,QAAM,QACJ,aAAa,UAAU,aAAa,cAAc,aAAa,YAAY,aAAa,WACnF,WAED,OAAO,aACL,aACA,OAAO,WACL,WACA;AAEV,SAAO,EAAE,OAAO,OAAO;AACzB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -561,6 +561,9 @@ __decorateClass([
|
|
|
561
561
|
__decorateClass([
|
|
562
562
|
Property({ name: "charge_type", type: "text" })
|
|
563
563
|
], CpqProductCharge.prototype, "chargeType", 2);
|
|
564
|
+
__decorateClass([
|
|
565
|
+
Property({ name: "charge_model", type: "text", nullable: true })
|
|
566
|
+
], CpqProductCharge.prototype, "chargeModel", 2);
|
|
564
567
|
__decorateClass([
|
|
565
568
|
Property({ name: "pricing_method", type: "text" })
|
|
566
569
|
], CpqProductCharge.prototype, "pricingMethod", 2);
|