@classytic/loyalty 0.4.0 → 0.6.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.
@@ -1,2 +1,2 @@
1
- import { n as createModels, t as LoyaltyModelCollisionError } from "../models-Ddh62lgO.mjs";
2
- export { LoyaltyModelCollisionError, createModels };
1
+ import { t as defineLoyaltyModels } from "../models-weWqS5Bc.mjs";
2
+ export { defineLoyaltyModels };
@@ -1,5 +1,7 @@
1
- import { a as MEMBER_STATUSES, c as RULE_STATUSES, l as RULE_TYPES, o as REDEMPTION_STATUSES, s as REFERRAL_STATUSES, u as TRANSACTION_TYPES } from "./constants-DkQYkOK1.mjs";
1
+ import { a as REFERRAL_STATUSES, c as TRANSACTION_TYPES, i as REDEMPTION_STATUSES, o as RULE_STATUSES, r as MEMBER_STATUSES, s as RULE_TYPES } from "./constants-DOPBh6kt.mjs";
2
+ import { defineModels } from "@classytic/mongokit";
2
3
  import mongoose from "mongoose";
4
+ import { mongooseMinorUnits } from "@classytic/validation/money";
3
5
  //#region src/models/create-model.ts
4
6
  /**
5
7
  * Injects the tenant field into a schema and (when tenant scoping is enabled
@@ -31,7 +33,7 @@ function injectTenantField(schema, tenant) {
31
33
  schema.add({ [tenant.tenantField]: {
32
34
  type: tenant.fieldType === "objectId" ? mongoose.Schema.Types.ObjectId : String,
33
35
  index: true,
34
- ...tenant.enabled ? { required: true } : {},
36
+ ...tenant.enabled && tenant.required ? { required: true } : {},
35
37
  ...tenant.ref ? { ref: tenant.ref } : {}
36
38
  } });
37
39
  if (!tenant.enabled) return;
@@ -83,7 +85,11 @@ function createEarningRuleSchema() {
83
85
  default: 0
84
86
  },
85
87
  conditions: {
86
- minOrderAmount: { type: Number },
88
+ minOrderAmount: {
89
+ type: Number,
90
+ min: 0,
91
+ validate: mongooseMinorUnits("conditions.minOrderAmount", { nullable: true })
92
+ },
87
93
  categories: [{ type: String }],
88
94
  tiers: [{ type: String }],
89
95
  actions: [{ type: String }],
@@ -94,8 +100,26 @@ function createEarningRuleSchema() {
94
100
  }
95
101
  },
96
102
  reward: {
97
- pointsPerAmount: { type: Number },
98
- amountPerPoint: { type: Number },
103
+ pointsPerAmount: {
104
+ type: Number,
105
+ min: 0
106
+ },
107
+ rateBasis: {
108
+ type: Number,
109
+ min: 1,
110
+ validate: {
111
+ validator: (v) => v === null || v === void 0 || Number.isInteger(v),
112
+ message: "reward.rateBasis must be an integer >= 1 (minor units) — got {VALUE}"
113
+ }
114
+ },
115
+ amountPerPoint: {
116
+ type: Number,
117
+ min: 1,
118
+ validate: {
119
+ validator: (v) => v === null || v === void 0 || Number.isInteger(v),
120
+ message: "reward.amountPerPoint must be an integer >= 1 (minor units per point) — got {VALUE}"
121
+ }
122
+ },
99
123
  fixedPoints: { type: Number },
100
124
  multiplier: { type: Number },
101
125
  roundingMode: {
@@ -113,6 +137,11 @@ function createEarningRuleSchema() {
113
137
  endsAt: { type: Date },
114
138
  metadata: { type: mongoose.Schema.Types.Mixed }
115
139
  }, { timestamps: true });
140
+ schema.pre("validate", function() {
141
+ const pointsPerAmount = this.get("reward.pointsPerAmount");
142
+ const rateBasis = this.get("reward.rateBasis");
143
+ if (pointsPerAmount !== null && pointsPerAmount !== void 0 && (rateBasis === null || rateBasis === void 0)) this.invalidate("reward.rateBasis", "reward.rateBasis (integer minor units) is required when reward.pointsPerAmount is set");
144
+ });
116
145
  schema.index({
117
146
  status: 1,
118
147
  type: 1,
@@ -282,11 +311,18 @@ function createRedemptionSchema() {
282
311
  },
283
312
  discountAmount: {
284
313
  type: Number,
285
- required: true
314
+ required: true,
315
+ min: 0,
316
+ validate: mongooseMinorUnits("discountAmount")
286
317
  },
287
- conversionRate: {
318
+ pointValueMinor: {
288
319
  type: Number,
289
- required: true
320
+ required: true,
321
+ min: 1,
322
+ validate: {
323
+ validator: Number.isInteger,
324
+ message: "pointValueMinor must be an integer >= 1 (minor units per point) — got {VALUE}"
325
+ }
290
326
  },
291
327
  status: {
292
328
  type: String,
@@ -308,8 +344,16 @@ function createRedemptionSchema() {
308
344
  reversalReason: { type: String },
309
345
  reversalTransactionId: { type: mongoose.Schema.Types.ObjectId },
310
346
  transactionId: { type: mongoose.Schema.Types.ObjectId },
311
- initialBalance: { type: Number },
312
- remainingBalance: { type: Number },
347
+ initialBalance: {
348
+ type: Number,
349
+ min: 0,
350
+ validate: mongooseMinorUnits("initialBalance", { nullable: true })
351
+ },
352
+ remainingBalance: {
353
+ type: Number,
354
+ min: 0,
355
+ validate: mongooseMinorUnits("remainingBalance", { nullable: true })
356
+ },
313
357
  approvals: {
314
358
  type: mongoose.Schema.Types.Mixed,
315
359
  default: null
@@ -394,7 +438,11 @@ function createTierDefinitionSchema() {
394
438
  },
395
439
  qualificationCriteria: {
396
440
  minLifetimePoints: { type: Number },
397
- minLifetimeSpend: { type: Number },
441
+ minLifetimeSpend: {
442
+ type: Number,
443
+ min: 0,
444
+ validate: mongooseMinorUnits("qualificationCriteria.minLifetimeSpend", { nullable: true })
445
+ },
398
446
  evaluationPeriodDays: { type: Number }
399
447
  },
400
448
  benefits: {
@@ -428,13 +476,6 @@ function createTierDefinitionSchema() {
428
476
  }
429
477
  //#endregion
430
478
  //#region src/models/index.ts
431
- function applyAutoIndex(models, autoIndex) {
432
- if (autoIndex === void 0) return;
433
- for (const [name, model] of Object.entries(models)) {
434
- const value = typeof autoIndex === "boolean" ? autoIndex : autoIndex[name] ?? void 0;
435
- if (value !== void 0) model.schema.set("autoIndex", value);
436
- }
437
- }
438
479
  /**
439
480
  * Default physical collection names (see PACKAGE_RULES.md §20.1). Prefixed
440
481
  * when `collectionPrefix` is provided; used verbatim when unset.
@@ -447,61 +488,102 @@ const DEFAULT_COLLECTIONS = {
447
488
  Redemption: "loyalty_redemptions",
448
489
  Referral: "loyalty_referrals"
449
490
  };
450
- const LOYALTY_MODEL_NAMES = [
451
- "LoyaltyMember",
452
- "LoyaltyPointTransaction",
453
- "LoyaltyEarningRule",
454
- "LoyaltyTierDefinition",
455
- "LoyaltyRedemption",
456
- "LoyaltyReferral"
457
- ];
458
- var LoyaltyModelCollisionError = class extends Error {
459
- constructor(name) {
460
- super(`[loyalty] Mongoose model "${name}" already exists on this connection. For hot-reload / test fixtures, pass \`forceRecreate: true\` to clobber the existing model. For two loyalty engines, use two Mongoose connections (\`mongoose.createConnection(...)\`) — each has its own model registry.`);
461
- this.name = "LoyaltyModelCollisionError";
462
- }
491
+ /** The registered Mongoose model names — stable, independent of `collectionPrefix`. */
492
+ const MODEL_NAMES = {
493
+ Member: "LoyaltyMember",
494
+ PointTransaction: "LoyaltyPointTransaction",
495
+ EarningRule: "LoyaltyEarningRule",
496
+ TierDefinition: "LoyaltyTierDefinition",
497
+ Redemption: "LoyaltyRedemption",
498
+ Referral: "LoyaltyReferral"
463
499
  };
464
- function createModels(connection, tenant, indexes, autoIndex, collectionPrefix, forceRecreate) {
465
- const memberSchema = createMemberSchema();
466
- const pointTransactionSchema = createPointTransactionSchema();
467
- const earningRuleSchema = createEarningRuleSchema();
468
- const tierDefinitionSchema = createTierDefinitionSchema();
469
- const redemptionSchema = createRedemptionSchema();
470
- const referralSchema = createReferralSchema();
471
- injectTenantField(memberSchema, tenant);
472
- injectTenantField(pointTransactionSchema, tenant);
473
- injectTenantField(earningRuleSchema, tenant);
474
- injectTenantField(tierDefinitionSchema, tenant);
475
- injectTenantField(redemptionSchema, tenant);
476
- injectTenantField(referralSchema, tenant);
477
- if (indexes) {
478
- const schemaMap = {
479
- member: memberSchema,
480
- pointTransaction: pointTransactionSchema,
481
- earningRule: earningRuleSchema,
482
- tierDefinition: tierDefinitionSchema,
483
- redemption: redemptionSchema,
484
- referral: referralSchema
485
- };
486
- for (const [modelName, defs] of Object.entries(indexes)) {
487
- const schema = schemaMap[modelName];
488
- if (schema && defs) applyUserIndexes(schema, defs, tenant);
489
- }
500
+ const ENTITIES = [
501
+ {
502
+ key: "Member",
503
+ modelName: MODEL_NAMES.Member,
504
+ collection: "Member",
505
+ indexKey: "member",
506
+ factory: createMemberSchema
507
+ },
508
+ {
509
+ key: "PointTransaction",
510
+ modelName: MODEL_NAMES.PointTransaction,
511
+ collection: "PointTransaction",
512
+ indexKey: "pointTransaction",
513
+ factory: createPointTransactionSchema
514
+ },
515
+ {
516
+ key: "EarningRule",
517
+ modelName: MODEL_NAMES.EarningRule,
518
+ collection: "EarningRule",
519
+ indexKey: "earningRule",
520
+ factory: createEarningRuleSchema
521
+ },
522
+ {
523
+ key: "TierDefinition",
524
+ modelName: MODEL_NAMES.TierDefinition,
525
+ collection: "TierDefinition",
526
+ indexKey: "tierDefinition",
527
+ factory: createTierDefinitionSchema
528
+ },
529
+ {
530
+ key: "Redemption",
531
+ modelName: MODEL_NAMES.Redemption,
532
+ collection: "Redemption",
533
+ indexKey: "redemption",
534
+ factory: createRedemptionSchema
535
+ },
536
+ {
537
+ key: "Referral",
538
+ modelName: MODEL_NAMES.Referral,
539
+ collection: "Referral",
540
+ indexKey: "referral",
541
+ factory: createReferralSchema
490
542
  }
491
- if (forceRecreate) {
492
- for (const name of LOYALTY_MODEL_NAMES) if (connection.models[name]) connection.deleteModel(name);
493
- } else for (const name of LOYALTY_MODEL_NAMES) if (connection.models[name]) throw new LoyaltyModelCollisionError(name);
543
+ ];
544
+ /**
545
+ * Describe the loyalty model set as a connection-free mongokit `ModelBlueprint`.
546
+ *
547
+ * Pure — builds no schema and registers nothing until `.bind(connection)`. This is the
548
+ * SINGLE authoritative place the loyalty models are described: tenant-field injection,
549
+ * user indexes, auto-index and collision policy all live here once (PACKAGE_RULES §20/§21).
550
+ * A name collision surfaces as mongokit's typed `ModelCollisionError`.
551
+ */
552
+ function defineLoyaltyModels(config) {
553
+ const { tenant, indexes, autoIndex, collectionPrefix, forceRecreate } = config;
494
554
  const prefix = collectionPrefix ?? "";
495
- const models = {
496
- Member: connection.model("LoyaltyMember", memberSchema, prefix + DEFAULT_COLLECTIONS.Member),
497
- PointTransaction: connection.model("LoyaltyPointTransaction", pointTransactionSchema, prefix + DEFAULT_COLLECTIONS.PointTransaction),
498
- EarningRule: connection.model("LoyaltyEarningRule", earningRuleSchema, prefix + DEFAULT_COLLECTIONS.EarningRule),
499
- TierDefinition: connection.model("LoyaltyTierDefinition", tierDefinitionSchema, prefix + DEFAULT_COLLECTIONS.TierDefinition),
500
- Redemption: connection.model("LoyaltyRedemption", redemptionSchema, prefix + DEFAULT_COLLECTIONS.Redemption),
501
- Referral: connection.model("LoyaltyReferral", referralSchema, prefix + DEFAULT_COLLECTIONS.Referral)
555
+ const resolveAutoIndex = (key) => {
556
+ if (autoIndex === void 0) return void 0;
557
+ return typeof autoIndex === "boolean" ? autoIndex : autoIndex[key];
502
558
  };
503
- applyAutoIndex(models, autoIndex);
504
- return models;
559
+ const existing = forceRecreate ? {
560
+ mode: "replace",
561
+ environment: "test"
562
+ } : { mode: "throw" };
563
+ return defineModels({
564
+ models: ENTITIES.map((entity) => ({
565
+ name: entity.modelName,
566
+ collection: prefix + DEFAULT_COLLECTIONS[entity.collection],
567
+ existing,
568
+ schema: () => {
569
+ const schema = entity.factory();
570
+ injectTenantField(schema, tenant);
571
+ const userIndexes = indexes?.[entity.indexKey];
572
+ if (userIndexes) applyUserIndexes(schema, userIndexes, tenant);
573
+ const ai = resolveAutoIndex(entity.key);
574
+ if (ai !== void 0) schema.set("autoIndex", ai);
575
+ return schema;
576
+ }
577
+ })),
578
+ assemble: (models) => Object.freeze({
579
+ Member: models.get(MODEL_NAMES.Member),
580
+ PointTransaction: models.get(MODEL_NAMES.PointTransaction),
581
+ EarningRule: models.get(MODEL_NAMES.EarningRule),
582
+ TierDefinition: models.get(MODEL_NAMES.TierDefinition),
583
+ Redemption: models.get(MODEL_NAMES.Redemption),
584
+ Referral: models.get(MODEL_NAMES.Referral)
585
+ })
586
+ });
505
587
  }
506
588
  //#endregion
507
- export { createModels as n, LoyaltyModelCollisionError as t };
589
+ export { defineLoyaltyModels as t };
@@ -1,5 +1,4 @@
1
- import { t as NotificationAdapter } from "../notification.port-CJ9nXj0k.mjs";
2
-
1
+ import { t as NotificationAdapter } from "../notification.port-wY4IKimz.mjs";
3
2
  //#region src/notifications/index.d.ts
4
3
  interface NotificationsAdapterConfig {
5
4
  service: {
@@ -1,2 +1,2 @@
1
- import { a as ReferralRepository, d as MemberRepository, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, r as createRepositories, s as RedemptionRepository, t as CreateRepositoriesInput, u as PointTransactionRepository } from "../index-Dngaj21_.mjs";
1
+ import { a as ReferralRepository, d as MemberRepository, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, r as createRepositories, s as RedemptionRepository, t as CreateRepositoriesInput, u as PointTransactionRepository } from "../index-CR2k7jQ8.mjs";
2
2
  export { CreateRepositoriesInput, EarningRuleRepository, LoyaltyRepositories, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
@@ -1,2 +1,2 @@
1
- import { a as PointTransactionRepository, i as RedemptionRepository, l as EarningRuleRepository, n as TierDefinitionRepository, o as MemberRepository, r as ReferralRepository, t as createRepositories } from "../repositories-DEwCbLCh.mjs";
1
+ import { a as PointTransactionRepository, i as RedemptionRepository, l as EarningRuleRepository, n as TierDefinitionRepository, o as MemberRepository, r as ReferralRepository, t as createRepositories } from "../repositories-B676z4bE.mjs";
2
2
  export { EarningRuleRepository, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };