@spinekit/promo 0.1.1 → 0.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.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as PromoPermissions, i as PromoModuleDeps, n as PromoActorCtx, r as PromoEngineLike, t as CrudGates } from "./types-D07igQha.mjs";
1
+ import { a as PromoPermissions, i as PromoModuleDeps, n as PromoActorCtx, r as PromoEngineLike, t as CrudGates } from "./types-ElFUq9u4.mjs";
2
2
  import { PromoFactoryCtx } from "./resources.mjs";
3
3
  //#region src/module.d.ts
4
4
  declare function createPromoModule<TEngine extends PromoEngineLike>(deps: PromoModuleDeps<TEngine>): import("@classytic/arc/factory").ArcModule<unknown>;
@@ -1,5 +1,6 @@
1
- import { r as PromoEngineLike } from "../types-D07igQha.mjs";
1
+ import { r as PromoEngineLike } from "../types-ElFUq9u4.mjs";
2
2
  import { EngineRef } from "@spinekit/kit/engine-slot";
3
+ import { PriceBasis } from "@classytic/primitives/price-basis";
3
4
  import { StructuredLogger } from "@classytic/primitives/context";
4
5
  //#region src/placement/promo-placement.d.ts
5
6
  /** Canonical line item shape for the promo engine. Matches `EvaluateInput.items`. */
@@ -26,7 +27,7 @@ interface ReservePromoInput {
26
27
  customerTags?: string[];
27
28
  /** Actor performing the placement (user, cashier, or 'system'). */
28
29
  actorId: string;
29
- /** Branch organization ID (BA org) — carried for audit only; promos are company-wide. */
30
+ /** Tenant organization when promo resources are tenant-scoped. */
30
31
  organizationId?: string;
31
32
  /**
32
33
  * Logger for structured error + audit reporting.
@@ -83,6 +84,7 @@ declare function buildPromoLines(resolvedLines: Array<{
83
84
  snapshot: {
84
85
  productId?: string;
85
86
  unitPrice?: number;
87
+ priceBasis?: PriceBasis;
86
88
  } | undefined;
87
89
  }>): PromoLineItem[];
88
90
  /** Subtotal from canonical lines — always drives the engine input. */
@@ -1,4 +1,5 @@
1
1
  import { resolveEngineRef } from "@spinekit/kit/engine-slot";
2
+ import { resolvePriceBasisTotal } from "@classytic/primitives/price-basis";
2
3
  //#region src/placement/promo-placement.ts
3
4
  const LOG_PREFIX = "[promo-placement]";
4
5
  const SYSTEM_ACTOR_ID = "system";
@@ -6,26 +7,24 @@ const resolveActorId = (actorId) => actorId && actorId.trim().length > 0 ? actor
6
7
  /**
7
8
  * Build the promo engine context.
8
9
  *
9
- * Promos are company-wide (see `promo.resources.ts` and the `tenant: false`
10
- * engine boot). Passing `organizationId` would make the repos inject a
11
- * per-branch filter on reads — programs seeded at company level would
12
- * become invisible to branch-scoped calls.
13
- *
14
- * The `organizationId` arriving into these helpers is carried only for
15
- * audit logging (which branch triggered the commit), never forwarded to
16
- * the engine itself.
10
+ * The organization scope is forwarded for shared-SaaS deployments and omitted
11
+ * by single-company hosts whose promo engine is global.
17
12
  */
18
- const buildCtx = (input) => ({ actorRef: input.actorId });
13
+ const buildCtx = (input) => ({
14
+ actorRef: input.actorId,
15
+ ...input.organizationId !== void 0 ? { organizationId: input.organizationId } : {}
16
+ });
19
17
  /** Canonical line builder for placement pipelines — see `placement.service.ts`. */
20
18
  function buildPromoLines(resolvedLines) {
21
19
  return resolvedLines.map((line) => {
22
20
  const unitPrice = line.snapshot?.unitPrice ?? 0;
21
+ const priceBasis = line.snapshot?.priceBasis;
23
22
  return {
24
23
  productId: line.snapshot?.productId ?? line.skuRef,
25
24
  sku: line.skuRef,
26
25
  quantity: line.quantity,
27
26
  unitPrice,
28
- lineTotal: unitPrice * line.quantity
27
+ lineTotal: priceBasis ? resolvePriceBasisTotal(unitPrice, line.quantity, priceBasis) : unitPrice * line.quantity
29
28
  };
30
29
  });
31
30
  }
@@ -52,7 +51,10 @@ async function reservePromo(input) {
52
51
  rejectedCodes: []
53
52
  };
54
53
  const actorId = resolveActorId(input.actorId);
55
- const ctx = buildCtx({ actorId });
54
+ const ctx = buildCtx({
55
+ actorId,
56
+ ...input.organizationId !== void 0 ? { organizationId: input.organizationId } : {}
57
+ });
56
58
  try {
57
59
  const result = await (await resolveEngineRef(input.engine)).services.evaluation.evaluate({
58
60
  items: input.lines,
@@ -101,7 +103,10 @@ async function commitPromo(reservation, orderId, input) {
101
103
  rejectedCodes: reservation.rejectedCodes
102
104
  };
103
105
  const actorId = resolveActorId(input.actorId);
104
- const ctx = buildCtx({ actorId });
106
+ const ctx = buildCtx({
107
+ actorId,
108
+ ...input.organizationId !== void 0 ? { organizationId: input.organizationId } : {}
109
+ });
105
110
  const evaluationId = reservation.evaluationId;
106
111
  try {
107
112
  await (await resolveEngineRef(input.engine)).services.evaluation.commit(evaluationId, orderId, ctx);
@@ -152,7 +157,10 @@ async function commitPromo(reservation, orderId, input) {
152
157
  async function rollbackPromo(reservation, input) {
153
158
  if (!reservation.evaluationId) return;
154
159
  const actorId = resolveActorId(input.actorId);
155
- const ctx = buildCtx({ actorId });
160
+ const ctx = buildCtx({
161
+ actorId,
162
+ ...input.organizationId !== void 0 ? { organizationId: input.organizationId } : {}
163
+ });
156
164
  try {
157
165
  await (await resolveEngineRef(input.engine)).services.evaluation.rollback(reservation.evaluationId, ctx);
158
166
  input.logger?.info?.({
@@ -1,4 +1,4 @@
1
- import { r as PromoEngineLike } from "../types-D07igQha.mjs";
1
+ import { r as PromoEngineLike } from "../types-ElFUq9u4.mjs";
2
2
  import { PromoPort } from "@classytic/promo";
3
3
  import { EngineRef } from "@spinekit/kit/engine-slot";
4
4
  //#region src/placement/promo.port.d.ts
@@ -1,4 +1,4 @@
1
- import { i as PromoModuleDeps, n as PromoActorCtx, r as PromoEngineLike } from "./types-D07igQha.mjs";
1
+ import { i as PromoModuleDeps, n as PromoActorCtx, r as PromoEngineLike } from "./types-ElFUq9u4.mjs";
2
2
  import { PromoModels, PromoRepositories, PromoServices } from "@classytic/promo";
3
3
  import { z } from "zod";
4
4
  import { FastifyRequest } from "fastify";
@@ -12,10 +12,10 @@ interface PromoFactoryCtx {
12
12
  actor: (req: FastifyRequest) => PromoActorCtx;
13
13
  }
14
14
  declare function buildFactoryCtx(deps: PromoModuleDeps, engine: PromoEngineLike): PromoFactoryCtx;
15
- declare function createProgramResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<unknown>;
16
- declare function createVoucherResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<unknown>;
17
- declare function createRuleResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<unknown>;
18
- declare function createRewardResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<unknown>;
15
+ declare function createProgramResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<import("@classytic/promo").Program>;
16
+ declare function createVoucherResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<import("@classytic/promo").Voucher>;
17
+ declare function createRuleResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<import("@classytic/promo").Reward>;
18
+ declare function createRewardResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<import("@classytic/promo").Reward>;
19
19
  declare function createEvaluationResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<unknown> & {
20
20
  readonly actions: {
21
21
  commit: {
@@ -1,7 +1,7 @@
1
1
  import { promoAction, promoRoute } from "./errors.mjs";
2
2
  import { ArcError } from "@classytic/arc/utils";
3
3
  import { defineResource, mergeResourceConfig } from "@classytic/arc";
4
- import { getUserId } from "@classytic/arc/scope";
4
+ import { scopeFirstCtx } from "@classytic/arc/scope";
5
5
  import { QueryParser } from "@classytic/mongokit";
6
6
  import { createMongooseAdapter } from "@classytic/mongokit/adapter";
7
7
  import { z } from "zod";
@@ -14,7 +14,11 @@ import { z } from "zod";
14
14
  * model) — id-scoped commit/rollback actions + preview/evaluate routes.
15
15
  */
16
16
  function defaultActor(req) {
17
- return { actorRef: getUserId(req.scope) ?? "anonymous" };
17
+ const scope = scopeFirstCtx(req);
18
+ return {
19
+ actorRef: scope.actorId ?? "anonymous",
20
+ ...scope.organizationId ? { organizationId: scope.organizationId } : {}
21
+ };
18
22
  }
19
23
  function buildFactoryCtx(deps, engine) {
20
24
  return {
@@ -36,7 +40,7 @@ function createProgramResource(fx) {
36
40
  displayName: "Promo Programs",
37
41
  tag: "Promotions",
38
42
  prefix: deps.prefixes?.program ?? "/promotions/programs",
39
- tenantField: false,
43
+ tenantField: deps.tenantField ?? false,
40
44
  adapter: createMongooseAdapter(engineModels.Program, engineRepos.program),
41
45
  permissions: {
42
46
  list: gates.list,
@@ -67,9 +71,12 @@ function createProgramResource(fx) {
67
71
  rawHandler: promoRoute(async (req) => {
68
72
  const { id } = req.params;
69
73
  const [program, rules, rewards] = await Promise.all([
70
- programRepo.getById(id, { lean: true }),
71
- ruleRepo.findAll({ programId: id }),
72
- rewardRepo.findAll({ programId: id })
74
+ programRepo.getById(id, {
75
+ lean: true,
76
+ ...actor(req)
77
+ }),
78
+ ruleRepo.findAll({ programId: id }, actor(req)),
79
+ rewardRepo.findAll({ programId: id }, actor(req))
73
80
  ]);
74
81
  if (!program) throw new ArcError("Program not found", {
75
82
  code: "PROGRAM_NOT_FOUND",
@@ -94,7 +101,7 @@ function createVoucherResource(fx) {
94
101
  displayName: "Promo Vouchers",
95
102
  tag: "Promotions",
96
103
  prefix: deps.prefixes?.voucher ?? "/promotions/vouchers",
97
- tenantField: false,
104
+ tenantField: deps.tenantField ?? false,
98
105
  adapter: createMongooseAdapter(engineModels.Voucher, engineRepos.voucher),
99
106
  /**
100
107
  * A voucher is STORED VALUE, and it is minted only by `/generate` and
@@ -160,7 +167,7 @@ function createVoucherResource(fx) {
160
167
  permissions: gates.get,
161
168
  rawHandler: promoRoute(async (req) => {
162
169
  const { code } = req.params;
163
- const data = await voucherRepo.getByCode?.(code);
170
+ const data = await voucherRepo.getByCode?.(code, actor(req));
164
171
  if (!data) throw new ArcError("Voucher not found", {
165
172
  code: "VOUCHER_NOT_FOUND",
166
173
  statusCode: 404
@@ -171,14 +178,25 @@ function createVoucherResource(fx) {
171
178
  ]
172
179
  }));
173
180
  }
181
+ /**
182
+ * `cfg` is a discriminated union on `modelKey`, not two independent
183
+ * `'Rule' | 'Reward'` / `'rule' | 'reward'` fields — those let TS pick
184
+ * `modelKey: 'Reward'` alongside `repoKey`'s WHOLE union (nothing ties the two
185
+ * together), and `createMongooseAdapter` then sees a repository argument
186
+ * typed `RuleRepository | RewardRepository` that no single
187
+ * `AdapterRepositoryInput<TDoc>` can satisfy. Branching on `cfg.modelKey`
188
+ * narrows the discriminated union so each branch's `repoKey` is a single
189
+ * literal, matching the model it is actually paired with.
190
+ */
174
191
  function crudResource(fx, cfg) {
192
+ const adapter = cfg.modelKey === "Rule" ? createMongooseAdapter(fx.engineModels.Rule, fx.engineRepos.rule) : createMongooseAdapter(fx.engineModels.Reward, fx.engineRepos.reward);
175
193
  return defineResource(mergeResourceConfig({
176
194
  name: cfg.name,
177
195
  displayName: cfg.displayName,
178
196
  tag: "Promotions",
179
197
  prefix: cfg.prefix,
180
- tenantField: false,
181
- adapter: createMongooseAdapter(fx.engineModels[cfg.modelKey], fx.engineRepos[cfg.repoKey]),
198
+ tenantField: fx.deps.tenantField ?? false,
199
+ adapter,
182
200
  queryParser: new QueryParser({
183
201
  maxLimit: 100,
184
202
  allowedFilterFields: ["programId", "code"],
@@ -69,6 +69,7 @@ interface PromoPermissions {
69
69
  /** Actor context for promo service calls (kernel `actorRef` convention). */
70
70
  interface PromoActorCtx {
71
71
  actorRef: string;
72
+ organizationId?: string;
72
73
  }
73
74
  interface PromoModuleDeps<TEngine extends PromoEngineLike = PromoEngineLike> {
74
75
  /**
@@ -115,6 +116,8 @@ interface PromoModuleDeps<TEngine extends PromoEngineLike = PromoEngineLike> {
115
116
  */
116
117
  onClose?: (fastify: unknown) => void | Promise<void>;
117
118
  permissions: PromoPermissions;
119
+ /** Arc resource tenancy. `false` preserves the company-wide default. */
120
+ tenantField?: string | false;
118
121
  /**
119
122
  * Derive the kernel actor context from the request. Default: arc's
120
123
  * scope-first actor (`request.scope` userId/clientId) falling back to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spinekit/promo",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Arc module for @classytic/promo — programs, vouchers, rules, rewards, and cart evaluation as composable arc resources. BYO engine; company-wide by default (tenantField: false — a voucher generated at one branch redeems at any other); ships the promo-kernel↔wire error contract (stable UPPER_SNAKE codes + HTTP statuses).",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -55,34 +55,35 @@
55
55
  "coupon",
56
56
  "evaluation"
57
57
  ],
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "typecheck": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
63
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
64
+ },
58
65
  "peerDependencies": {
59
- "@classytic/arc": ">=2.34.0",
60
- "@classytic/mongokit": ">=3.34.0",
61
- "@classytic/primitives": ">=0.23.0",
62
- "@classytic/promo": ">=0.7.0",
63
- "@spinekit/kit": ">=0.1.0",
64
- "mongoose": ">=9.4.1",
65
- "zod": ">=4.0.0"
66
+ "@classytic/arc": ">=2.38.0",
67
+ "@classytic/mongokit": ">=3.36.0",
68
+ "@classytic/primitives": ">=0.26.4",
69
+ "@classytic/promo": ">=0.7.1",
70
+ "@spinekit/kit": ">=0.4.0",
71
+ "mongoose": ">=9.9.4",
72
+ "zod": ">=4.5.4"
66
73
  },
67
74
  "devDependencies": {
68
- "@classytic/arc": "^2.34.0",
75
+ "@classytic/arc": "^2.38.0",
69
76
  "@classytic/arc-testkit": "^0.4.0",
70
- "@classytic/mongokit": ">=3.34.0",
71
- "@classytic/primitives": ">=0.23.0",
72
- "@classytic/promo": ">=0.7.0",
77
+ "@classytic/mongokit": "^3.36.0",
78
+ "@classytic/primitives": ">=0.26.4",
79
+ "@classytic/promo": ">=0.7.1",
80
+ "@spinekit/kit": "workspace:*",
73
81
  "@types/node": "^24.3.0",
74
82
  "fastify": "^5.12.0",
75
- "mongoose": "^9.7.2",
83
+ "mongoose": "^9.9.4",
76
84
  "tsdown": "^0.22.14",
77
85
  "typescript": "^7.0.2",
78
86
  "vitest": "^3.2.4",
79
- "zod": "^4.3.6",
80
- "@spinekit/kit": "0.1.0"
81
- },
82
- "scripts": {
83
- "build": "tsdown",
84
- "typecheck": "tsc --noEmit",
85
- "test": "vitest run",
86
- "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
87
+ "zod": "^4.5.4"
87
88
  }
88
- }
89
+ }