@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 +1 -1
- package/dist/placement/promo-placement.d.mts +4 -2
- package/dist/placement/promo-placement.mjs +21 -13
- package/dist/placement/promo.port.d.mts +1 -1
- package/dist/resources.d.mts +5 -5
- package/dist/resources.mjs +28 -10
- package/dist/{types-D07igQha.d.mts → types-ElFUq9u4.d.mts} +3 -0
- package/package.json +23 -22
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-
|
|
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-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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) => ({
|
|
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({
|
|
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({
|
|
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({
|
|
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-
|
|
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
|
package/dist/resources.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as PromoModuleDeps, n as PromoActorCtx, r as PromoEngineLike } from "./types-
|
|
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<
|
|
16
|
-
declare function createVoucherResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<
|
|
17
|
-
declare function createRuleResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<
|
|
18
|
-
declare function createRewardResource(fx: PromoFactoryCtx): import("@classytic/arc").ResourceDefinition<
|
|
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: {
|
package/dist/resources.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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, {
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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.
|
|
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.
|
|
60
|
-
"@classytic/mongokit": ">=3.
|
|
61
|
-
"@classytic/primitives": ">=0.
|
|
62
|
-
"@classytic/promo": ">=0.7.
|
|
63
|
-
"@spinekit/kit": ">=0.
|
|
64
|
-
"mongoose": ">=9.4
|
|
65
|
-
"zod": ">=4.
|
|
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.
|
|
75
|
+
"@classytic/arc": "^2.38.0",
|
|
69
76
|
"@classytic/arc-testkit": "^0.4.0",
|
|
70
|
-
"@classytic/mongokit": "
|
|
71
|
-
"@classytic/primitives": ">=0.
|
|
72
|
-
"@classytic/promo": ">=0.7.
|
|
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.
|
|
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.
|
|
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
|
+
}
|