@happyvertical/smrt-sales 0.40.9 → 0.40.11

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 +1 @@
1
- {"version":3,"file":"__smrt-register__-D08SaNJE.js","names":[],"sources":["../../src/__smrt-register__.ts"],"sourcesContent":["import { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n"],"mappings":""}
1
+ {"version":3,"file":"__smrt-register__-CPynA4ag.js","names":[],"sources":["../../src/__smrt-register__.ts"],"sourcesContent":["import { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n"],"mappings":""}
@@ -1,14 +1,14 @@
1
1
  import { P as CommissionCollection, i as CommissionCalculationService, y as CommissionPlanCollection } from "./commissions-CelWwvjZ.js";
2
2
  import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
3
3
  import { TenantScoped, requireTenantId, tenantId } from "@happyvertical/smrt-tenancy";
4
- import { randomBytes, randomUUID } from "node:crypto";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  //#region src/referrals/models/AttributionException.ts
6
- var __defProp$8 = Object.defineProperty;
7
- var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
8
- var __decorateClass$8 = (decorators, target, key, kind) => {
9
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$8(target, key) : target;
6
+ var __defProp$9 = Object.defineProperty;
7
+ var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
8
+ var __decorateClass$9 = (decorators, target, key, kind) => {
9
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$9(target, key) : target;
10
10
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
11
- if (kind && result) __defProp$8(target, key, result);
11
+ if (kind && result) __defProp$9(target, key, result);
12
12
  return result;
13
13
  };
14
14
  var AttributionException = class extends SmrtObject {
@@ -146,12 +146,12 @@ var AttributionException = class extends SmrtObject {
146
146
  return null;
147
147
  }
148
148
  };
149
- __decorateClass$8([tenantId({ nullable: true })], AttributionException.prototype, "tenantId", 2);
150
- __decorateClass$8([field({ required: true })], AttributionException.prototype, "targetKind", 2);
151
- __decorateClass$8([field({ required: true })], AttributionException.prototype, "targetId", 2);
152
- __decorateClass$8([foreignKey("ReferralProgram")], AttributionException.prototype, "programId", 2);
153
- __decorateClass$8([crossPackageRef("@happyvertical/smrt-profiles:Profile")], AttributionException.prototype, "resolvedByProfileId", 2);
154
- AttributionException = __decorateClass$8([TenantScoped({ mode: "optional" }), smrt({
149
+ __decorateClass$9([tenantId({ nullable: true })], AttributionException.prototype, "tenantId", 2);
150
+ __decorateClass$9([field({ required: true })], AttributionException.prototype, "targetKind", 2);
151
+ __decorateClass$9([field({ required: true })], AttributionException.prototype, "targetId", 2);
152
+ __decorateClass$9([foreignKey("ReferralProgram")], AttributionException.prototype, "programId", 2);
153
+ __decorateClass$9([crossPackageRef("@happyvertical/smrt-profiles:Profile")], AttributionException.prototype, "resolvedByProfileId", 2);
154
+ AttributionException = __decorateClass$9([TenantScoped({ mode: "optional" }), smrt({
155
155
  api: { include: ["list", "get"] },
156
156
  mcp: { include: ["list", "get"] },
157
157
  cli: false
@@ -247,12 +247,12 @@ var REFERRAL_AGREEMENT_STATUSES = [
247
247
  var REFERRAL_AGREEMENT_APPROVAL_MODES = ["manual", "auto"];
248
248
  //#endregion
249
249
  //#region src/referrals/models/AttributionPolicy.ts
250
- var __defProp$7 = Object.defineProperty;
251
- var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
252
- var __decorateClass$7 = (decorators, target, key, kind) => {
253
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
250
+ var __defProp$8 = Object.defineProperty;
251
+ var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
252
+ var __decorateClass$8 = (decorators, target, key, kind) => {
253
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$8(target, key) : target;
254
254
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
255
- if (kind && result) __defProp$7(target, key, result);
255
+ if (kind && result) __defProp$8(target, key, result);
256
256
  return result;
257
257
  };
258
258
  var POLICY_STATUS_TRANSITIONS = {
@@ -519,9 +519,9 @@ var AttributionPolicy = class extends SmrtObject {
519
519
  return null;
520
520
  }
521
521
  };
522
- __decorateClass$7([tenantId({ nullable: true })], AttributionPolicy.prototype, "tenantId", 2);
523
- __decorateClass$7([field({ required: true })], AttributionPolicy.prototype, "policyKey", 2);
524
- AttributionPolicy = __decorateClass$7([TenantScoped({ mode: "optional" }), smrt({
522
+ __decorateClass$8([tenantId({ nullable: true })], AttributionPolicy.prototype, "tenantId", 2);
523
+ __decorateClass$8([field({ required: true })], AttributionPolicy.prototype, "policyKey", 2);
524
+ AttributionPolicy = __decorateClass$8([TenantScoped({ mode: "optional" }), smrt({
525
525
  conflictColumns: [
526
526
  "tenant_id",
527
527
  "policy_key",
@@ -605,12 +605,12 @@ var AttributionPolicyCollection = class extends SmrtCollection {
605
605
  };
606
606
  //#endregion
607
607
  //#region src/referrals/models/ReferralAgreement.ts
608
- var __defProp$6 = Object.defineProperty;
609
- var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
610
- var __decorateClass$6 = (decorators, target, key, kind) => {
611
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
608
+ var __defProp$7 = Object.defineProperty;
609
+ var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
610
+ var __decorateClass$7 = (decorators, target, key, kind) => {
611
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
612
612
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
613
- if (kind && result) __defProp$6(target, key, result);
613
+ if (kind && result) __defProp$7(target, key, result);
614
614
  return result;
615
615
  };
616
616
  var AGREEMENT_STATUS_TRANSITIONS = {
@@ -847,12 +847,12 @@ var ReferralAgreement = class extends SmrtObject {
847
847
  return null;
848
848
  }
849
849
  };
850
- __decorateClass$6([tenantId({ nullable: true })], ReferralAgreement.prototype, "tenantId", 2);
851
- __decorateClass$6([foreignKey("Referrer", { required: true })], ReferralAgreement.prototype, "referrerId", 2);
852
- __decorateClass$6([foreignKey("ReferralProgram", { required: true })], ReferralAgreement.prototype, "programId", 2);
853
- __decorateClass$6([foreignKey("AgreementExecution")], ReferralAgreement.prototype, "executionId", 2);
854
- __decorateClass$6([foreignKey("ExecutedAgreement")], ReferralAgreement.prototype, "executedAgreementId", 2);
855
- ReferralAgreement = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
850
+ __decorateClass$7([tenantId({ nullable: true })], ReferralAgreement.prototype, "tenantId", 2);
851
+ __decorateClass$7([foreignKey("Referrer", { required: true })], ReferralAgreement.prototype, "referrerId", 2);
852
+ __decorateClass$7([foreignKey("ReferralProgram", { required: true })], ReferralAgreement.prototype, "programId", 2);
853
+ __decorateClass$7([foreignKey("AgreementExecution")], ReferralAgreement.prototype, "executionId", 2);
854
+ __decorateClass$7([foreignKey("ExecutedAgreement")], ReferralAgreement.prototype, "executedAgreementId", 2);
855
+ ReferralAgreement = __decorateClass$7([TenantScoped({ mode: "optional" }), smrt({
856
856
  conflictColumns: [
857
857
  "referrer_id",
858
858
  "program_id",
@@ -931,12 +931,12 @@ var ReferralAgreementCollection = class extends SmrtCollection {
931
931
  };
932
932
  //#endregion
933
933
  //#region src/referrals/models/Referral.ts
934
- var __defProp$5 = Object.defineProperty;
935
- var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
936
- var __decorateClass$5 = (decorators, target, key, kind) => {
937
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
934
+ var __defProp$6 = Object.defineProperty;
935
+ var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
936
+ var __decorateClass$6 = (decorators, target, key, kind) => {
937
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
938
938
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
939
- if (kind && result) __defProp$5(target, key, result);
939
+ if (kind && result) __defProp$6(target, key, result);
940
940
  return result;
941
941
  };
942
942
  var REFERRAL_STATUS_TRANSITIONS = {
@@ -1143,11 +1143,11 @@ var Referral = class extends SmrtObject {
1143
1143
  return null;
1144
1144
  }
1145
1145
  };
1146
- __decorateClass$5([tenantId({ nullable: true })], Referral.prototype, "tenantId", 2);
1147
- __decorateClass$5([foreignKey("Referrer", { required: true })], Referral.prototype, "referrerId", 2);
1148
- __decorateClass$5([foreignKey("ReferralProgram", { required: true })], Referral.prototype, "programId", 2);
1149
- __decorateClass$5([foreignKey("ReferralTouch")], Referral.prototype, "primaryTouchId", 2);
1150
- Referral = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
1146
+ __decorateClass$6([tenantId({ nullable: true })], Referral.prototype, "tenantId", 2);
1147
+ __decorateClass$6([foreignKey("Referrer", { required: true })], Referral.prototype, "referrerId", 2);
1148
+ __decorateClass$6([foreignKey("ReferralProgram", { required: true })], Referral.prototype, "programId", 2);
1149
+ __decorateClass$6([foreignKey("ReferralTouch")], Referral.prototype, "primaryTouchId", 2);
1150
+ Referral = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
1151
1151
  api: { include: [
1152
1152
  "list",
1153
1153
  "get",
@@ -1217,12 +1217,12 @@ var ReferralCollection = class extends SmrtCollection {
1217
1217
  };
1218
1218
  //#endregion
1219
1219
  //#region src/referrals/models/ReferralLink.ts
1220
- var __defProp$4 = Object.defineProperty;
1221
- var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
1222
- var __decorateClass$4 = (decorators, target, key, kind) => {
1223
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
1220
+ var __defProp$5 = Object.defineProperty;
1221
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
1222
+ var __decorateClass$5 = (decorators, target, key, kind) => {
1223
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
1224
1224
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1225
- if (kind && result) __defProp$4(target, key, result);
1225
+ if (kind && result) __defProp$5(target, key, result);
1226
1226
  return result;
1227
1227
  };
1228
1228
  function assertHttpTargetUrl(targetUrl) {
@@ -1308,11 +1308,11 @@ var ReferralLink = class extends SmrtObject {
1308
1308
  return await super.save();
1309
1309
  }
1310
1310
  };
1311
- __decorateClass$4([tenantId({ nullable: true })], ReferralLink.prototype, "tenantId", 2);
1312
- __decorateClass$4([foreignKey("Referrer", { required: true })], ReferralLink.prototype, "referrerId", 2);
1313
- __decorateClass$4([foreignKey("ReferralProgram", { required: true })], ReferralLink.prototype, "programId", 2);
1314
- __decorateClass$4([field({ required: true })], ReferralLink.prototype, "code", 2);
1315
- ReferralLink = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
1311
+ __decorateClass$5([tenantId({ nullable: true })], ReferralLink.prototype, "tenantId", 2);
1312
+ __decorateClass$5([foreignKey("Referrer", { required: true })], ReferralLink.prototype, "referrerId", 2);
1313
+ __decorateClass$5([foreignKey("ReferralProgram", { required: true })], ReferralLink.prototype, "programId", 2);
1314
+ __decorateClass$5([field({ required: true })], ReferralLink.prototype, "code", 2);
1315
+ ReferralLink = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
1316
1316
  conflictColumns: ["code"],
1317
1317
  api: { include: [
1318
1318
  "list",
@@ -1328,6 +1328,113 @@ ReferralLink = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
1328
1328
  cli: true
1329
1329
  })], ReferralLink);
1330
1330
  //#endregion
1331
+ //#region src/referrals/models/ReferralClickOperation.ts
1332
+ var __defProp$4 = Object.defineProperty;
1333
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
1334
+ var __decorateClass$4 = (decorators, target, key, kind) => {
1335
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
1336
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1337
+ if (kind && result) __defProp$4(target, key, result);
1338
+ return result;
1339
+ };
1340
+ var ReferralClickOperation = class extends SmrtObject {
1341
+ tenantId = null;
1342
+ keyHash;
1343
+ linkId;
1344
+ touchId;
1345
+ intentHash;
1346
+ callerEvidenceHash;
1347
+ requestedOccurredAt;
1348
+ constructor(options = {}) {
1349
+ super(options);
1350
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1351
+ if (options.keyHash !== void 0) this.keyHash = options.keyHash;
1352
+ if (options.linkId !== void 0) this.linkId = options.linkId;
1353
+ if (options.touchId !== void 0) this.touchId = options.touchId;
1354
+ if (options.intentHash !== void 0) this.intentHash = options.intentHash;
1355
+ if (options.callerEvidenceHash !== void 0) this.callerEvidenceHash = options.callerEvidenceHash;
1356
+ if (options.requestedOccurredAt !== void 0) this.requestedOccurredAt = options.requestedOccurredAt;
1357
+ }
1358
+ /** Keep the TEXT replay marker a string when adapters parse `*_at`. */
1359
+ async initialize() {
1360
+ await super.initialize();
1361
+ const value = this.requestedOccurredAt;
1362
+ if (value instanceof Date) this.requestedOccurredAt = value.toISOString();
1363
+ return this;
1364
+ }
1365
+ };
1366
+ __decorateClass$4([tenantId({ nullable: true })], ReferralClickOperation.prototype, "tenantId", 2);
1367
+ __decorateClass$4([field({
1368
+ required: true,
1369
+ readonly: true
1370
+ })], ReferralClickOperation.prototype, "keyHash", 2);
1371
+ __decorateClass$4([field({
1372
+ sqlType: "UUID",
1373
+ required: true,
1374
+ readonly: true,
1375
+ indexed: true
1376
+ })], ReferralClickOperation.prototype, "linkId", 2);
1377
+ __decorateClass$4([field({
1378
+ sqlType: "UUID",
1379
+ required: true,
1380
+ readonly: true,
1381
+ indexed: true
1382
+ })], ReferralClickOperation.prototype, "touchId", 2);
1383
+ __decorateClass$4([field({
1384
+ required: true,
1385
+ readonly: true
1386
+ })], ReferralClickOperation.prototype, "intentHash", 2);
1387
+ __decorateClass$4([field({
1388
+ required: true,
1389
+ readonly: true
1390
+ })], ReferralClickOperation.prototype, "callerEvidenceHash", 2);
1391
+ __decorateClass$4([field({
1392
+ required: true,
1393
+ readonly: true
1394
+ })], ReferralClickOperation.prototype, "requestedOccurredAt", 2);
1395
+ ReferralClickOperation = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
1396
+ api: false,
1397
+ mcp: false,
1398
+ cli: false
1399
+ })], ReferralClickOperation);
1400
+ //#endregion
1401
+ //#region src/referrals/collections/ReferralClickOperationCollection.ts
1402
+ var ReferralClickOperationCollection = class extends SmrtCollection {
1403
+ static _itemClass = ReferralClickOperation;
1404
+ /** Tenant-filtered lookup; a foreign operation payload remains invisible. */
1405
+ async findByOperationId(operationId) {
1406
+ return await this.get({ id: operationId }, { cache: false });
1407
+ }
1408
+ /**
1409
+ * Claim the deterministic operation UUID without changing a winner.
1410
+ * Must run in the transaction that creates the touch and increments the
1411
+ * link counter.
1412
+ */
1413
+ async claim(input) {
1414
+ const inserted = await this.query(`INSERT INTO ${this.tableName} (
1415
+ id, slug, context, tenant_id, key_hash, link_id, touch_id,
1416
+ intent_hash, caller_evidence_hash, requested_occurred_at
1417
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1418
+ ON CONFLICT (id) DO NOTHING
1419
+ RETURNING id`, [
1420
+ input.operationId,
1421
+ input.operationId,
1422
+ "",
1423
+ input.tenantId,
1424
+ input.keyHash,
1425
+ input.linkId,
1426
+ input.touchId,
1427
+ input.intentHash,
1428
+ input.callerEvidenceHash,
1429
+ input.requestedOccurredAt
1430
+ ], { allowRawOnTenantScoped: true });
1431
+ return {
1432
+ operation: await this.findByOperationId(input.operationId),
1433
+ claimed: inserted.length === 1
1434
+ };
1435
+ }
1436
+ };
1437
+ //#endregion
1331
1438
  //#region src/referrals/models/ReferralTouch.ts
1332
1439
  var __defProp$3 = Object.defineProperty;
1333
1440
  var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
@@ -1487,6 +1594,8 @@ var ReferralTouchCollection = class extends SmrtCollection {
1487
1594
  var REFERRAL_CODE_LENGTH = 10;
1488
1595
  var REFERRAL_CODE_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
1489
1596
  var MAX_CODE_GENERATION_ATTEMPTS = 5;
1597
+ var DEFAULT_REFERRAL_CLICK_EVIDENCE_MAX_BYTES = 4096;
1598
+ var MAX_REFERRAL_CLICK_IDEMPOTENCY_KEY_BYTES = 256;
1490
1599
  function generateReferralCode() {
1491
1600
  const alphabetSize = 36;
1492
1601
  const rejectionBound = 256 - 256 % alphabetSize;
@@ -1501,7 +1610,29 @@ function generateReferralCode() {
1501
1610
  }
1502
1611
  return code;
1503
1612
  }
1504
- var ReferralLinkCollection = class extends SmrtCollection {
1613
+ var ReferralClickReplayConflictError = class extends Error {
1614
+ constructor(idempotencyKey, mismatches) {
1615
+ super(`Referral click idempotency key '${idempotencyKey}' was already used with different ${mismatches.length === 1 ? "intent field" : "intent fields"}: ` + mismatches.join(", "));
1616
+ this.idempotencyKey = idempotencyKey;
1617
+ this.mismatches = mismatches;
1618
+ this.name = "ReferralClickReplayConflictError";
1619
+ }
1620
+ idempotencyKey;
1621
+ mismatches;
1622
+ code = "REFERRAL_CLICK_REPLAY_CONFLICT";
1623
+ };
1624
+ var ReferralClickValidationError = class extends Error {
1625
+ constructor(reason, message, details) {
1626
+ super(message);
1627
+ this.reason = reason;
1628
+ this.details = details;
1629
+ this.name = "ReferralClickValidationError";
1630
+ }
1631
+ reason;
1632
+ details;
1633
+ code = "REFERRAL_CLICK_VALIDATION_ERROR";
1634
+ };
1635
+ var ReferralLinkCollection = class ReferralLinkCollection extends SmrtCollection {
1505
1636
  static _itemClass = ReferralLink;
1506
1637
  /** All links for a referrer, newest first. */
1507
1638
  async findByReferrer(referrerId) {
@@ -1552,10 +1683,16 @@ var ReferralLinkCollection = class extends SmrtCollection {
1552
1683
  throw new Error(`ReferralLinkCollection.createWithUniqueCode: could not mint a unique code after 5 attempts \u2014 the code generator is returning colliding codes`);
1553
1684
  }
1554
1685
  /**
1555
- * Record a click on a share code: find the ACTIVE link, increment its
1556
- * {@link ReferralLink.clickCount}, and write an immutable ReferralTouch
1557
- * (kind `'click'`, evidence carrying the code/link/url plus any caller
1558
- * evidence). Returns `{ link, touch }` on success.
1686
+ * Record a click on a share code. A private operation fence, immutable
1687
+ * ReferralTouch, and {@link ReferralLink.clickCount} increment commit in
1688
+ * one supported database transaction. An exact `idempotencyKey` replay
1689
+ * returns the original touch without another increment; changed immutable
1690
+ * intent raises {@link ReferralClickReplayConflictError}.
1691
+ *
1692
+ * Evidence is canonicalized, Sales-owned `code`/`linkId`/`targetUrl` fields
1693
+ * are applied, and the UTF-8 bytes of that exact persisted JSON are checked
1694
+ * before any write. The default bound is
1695
+ * {@link DEFAULT_REFERRAL_CLICK_EVIDENCE_MAX_BYTES}.
1559
1696
  *
1560
1697
  * Refusals return a typed result instead of throwing (clicks arrive from
1561
1698
  * the edge — an unknown or disabled code is an expected outcome, not an
@@ -1564,7 +1701,41 @@ var ReferralLinkCollection = class extends SmrtCollection {
1564
1701
  * returned, nothing is written).
1565
1702
  */
1566
1703
  async recordClick(input) {
1567
- const link = await this.findByCode(input.code);
1704
+ const request = canonicalizeClickRequest(input);
1705
+ const db = this.db;
1706
+ if (typeof db.transaction !== "function") throw new ReferralClickValidationError("transaction_unavailable", "Referral click recording requires a transaction-capable database adapter");
1707
+ const transactionResult = await db.transaction(async (tx) => {
1708
+ const deps = {
1709
+ links: await ReferralLinkCollection.create({ db: tx }),
1710
+ touches: await ReferralTouchCollection.create({ db: tx }),
1711
+ operations: await ReferralClickOperationCollection.create({ db: tx })
1712
+ };
1713
+ return await this.recordClickInTransaction(deps, request);
1714
+ });
1715
+ return await this.rehydrateRecordClickResult(transactionResult);
1716
+ }
1717
+ /** Rebind public result models to the caller's database after commit. */
1718
+ async rehydrateRecordClickResult(result) {
1719
+ let link = null;
1720
+ if (result.link?.id) {
1721
+ link = await this.get({ id: result.link.id }, { cache: false });
1722
+ if (!link) throw new ReferralClickValidationError("operation_link_missing", "Committed referral click result has no visible link");
1723
+ }
1724
+ let touch = null;
1725
+ if (result.touch?.id) {
1726
+ touch = await (await ReferralTouchCollection.create({ db: this.db })).get({ id: result.touch.id }, { cache: false });
1727
+ if (!touch) throw new ReferralClickValidationError("operation_touch_missing", "Committed referral click result has no visible touch");
1728
+ }
1729
+ return {
1730
+ ...result,
1731
+ link,
1732
+ touch
1733
+ };
1734
+ }
1735
+ async recordClickInTransaction(deps, request) {
1736
+ const existingOperation = await deps.operations.findByOperationId(request.operationId);
1737
+ if (existingOperation) return await this.replayClick(deps, existingOperation, request);
1738
+ const link = await deps.links.findByCode(request.normalizedCode);
1568
1739
  if (!link) return {
1569
1740
  link: null,
1570
1741
  touch: null,
@@ -1575,31 +1746,215 @@ var ReferralLinkCollection = class extends SmrtCollection {
1575
1746
  touch: null,
1576
1747
  refused: "link_disabled"
1577
1748
  };
1578
- const occurredAt = input.occurredAt ?? /* @__PURE__ */ new Date();
1579
- const touch = await (await ReferralTouchCollection.create({ db: this.db })).create({
1749
+ if (!link.id) throw new ReferralClickValidationError("link_update_conflict", "Referral click link is missing its persisted identifier");
1750
+ const intent = buildClickIntent(link, request);
1751
+ assertEvidenceBound(intent.finalEvidence, request.maxEvidenceBytes);
1752
+ const touchId = randomUUID();
1753
+ const claimed = await deps.operations.claim({
1754
+ operationId: request.operationId,
1580
1755
  tenantId: link.tenantId,
1581
- linkId: link.id ?? "",
1756
+ keyHash: request.keyHash,
1757
+ linkId: link.id,
1758
+ touchId,
1759
+ intentHash: intent.intentHash,
1760
+ callerEvidenceHash: request.callerEvidenceHash,
1761
+ requestedOccurredAt: request.requestedOccurredAt
1762
+ });
1763
+ if (!claimed.operation) throw new ReferralClickReplayConflictError(request.idempotencyKey, ["tenantId"]);
1764
+ if (!claimed.claimed) return await this.replayClick(deps, claimed.operation, request);
1765
+ const touch = await deps.touches.create({
1766
+ id: touchId,
1767
+ tenantId: link.tenantId,
1768
+ linkId: link.id,
1582
1769
  code: link.code,
1583
1770
  referrerId: link.referrerId,
1584
1771
  programId: link.programId,
1585
1772
  kind: "click",
1586
- subjectKind: input.subjectKind ?? "",
1587
- subjectId: input.subjectId ?? "",
1588
- occurredAt,
1589
- evidence: JSON.stringify({
1590
- ...input.evidence ?? {},
1591
- code: link.code,
1592
- linkId: link.id ?? "",
1593
- targetUrl: link.targetUrl
1594
- })
1773
+ subjectKind: request.subjectKind,
1774
+ subjectId: request.subjectId,
1775
+ occurredAt: request.occurredAt ?? /* @__PURE__ */ new Date(),
1776
+ evidence: intent.finalEvidence
1595
1777
  });
1596
- await this.db.execute`UPDATE referral_links SET click_count = click_count + 1 WHERE id = ${link.id ?? ""}`;
1778
+ if ((await deps.links.query(`UPDATE ${deps.links.tableName}
1779
+ SET click_count = click_count + 1
1780
+ WHERE id = ?
1781
+ AND status = 'active'
1782
+ AND COALESCE(CAST(tenant_id AS TEXT), '') = ?
1783
+ RETURNING id`, [link.id, link.tenantId ?? ""], { allowRawOnTenantScoped: true })).length !== 1) throw new ReferralClickValidationError("link_update_conflict", "Referral link changed or was disabled while its click was being recorded; the transaction was rolled back");
1784
+ const fresh = await deps.links.get({ id: link.id }, { cache: false });
1785
+ if (!fresh || !sameClickLinkSnapshot(link, fresh)) throw new ReferralClickValidationError("link_update_conflict", "Referral link attribution changed while its click was being recorded; the transaction was rolled back");
1597
1786
  return {
1598
- link: await this.get({ id: link.id }) ?? link,
1599
- touch
1787
+ link: fresh,
1788
+ touch,
1789
+ idempotencyKey: request.idempotencyKey,
1790
+ replayed: false
1791
+ };
1792
+ }
1793
+ async replayClick(deps, operation, request) {
1794
+ if (operation.keyHash !== request.keyHash) throw new ReferralClickReplayConflictError(request.idempotencyKey, ["idempotencyKey"]);
1795
+ const link = await deps.links.get({ id: operation.linkId }, { cache: false });
1796
+ if (!link) throw new ReferralClickValidationError("operation_link_missing", `Referral click operation '${request.idempotencyKey}' has no visible link`);
1797
+ const touch = await deps.touches.get({ id: operation.touchId }, { cache: false });
1798
+ if (!touch) throw new ReferralClickValidationError("operation_touch_missing", `Referral click operation '${request.idempotencyKey}' has no visible touch`);
1799
+ const mismatches = collectReplayMismatches(operation, link, touch, request);
1800
+ if (mismatches.length > 0) throw new ReferralClickReplayConflictError(request.idempotencyKey, mismatches);
1801
+ return {
1802
+ link,
1803
+ touch,
1804
+ idempotencyKey: request.idempotencyKey,
1805
+ replayed: true
1600
1806
  };
1601
1807
  }
1602
1808
  };
1809
+ function canonicalizeClickRequest(input) {
1810
+ const idempotencyKey = input.idempotencyKey ?? randomUUID();
1811
+ if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0 || idempotencyKey.trim() !== idempotencyKey || !isWellFormedUnicode(idempotencyKey) || utf8ByteLength(idempotencyKey) > 256) throw new ReferralClickValidationError("invalid_idempotency_key", `Referral click idempotencyKey must be non-empty, unpadded, well-formed Unicode, and at most 256 UTF-8 bytes`);
1812
+ const maxEvidenceBytes = input.maxEvidenceBytes ?? 4096;
1813
+ if (!Number.isSafeInteger(maxEvidenceBytes) || maxEvidenceBytes <= 0) throw new ReferralClickValidationError("invalid_max_evidence_bytes", "Referral click maxEvidenceBytes must be a positive safe integer");
1814
+ let requestedOccurredAt = "";
1815
+ let occurredAt = null;
1816
+ if (input.occurredAt !== void 0) {
1817
+ if (!(input.occurredAt instanceof Date) || Number.isNaN(input.occurredAt.getTime())) throw new ReferralClickValidationError("invalid_occurred_at", "Referral click occurredAt must be a valid Date");
1818
+ occurredAt = new Date(input.occurredAt.getTime());
1819
+ requestedOccurredAt = occurredAt.toISOString();
1820
+ }
1821
+ const callerEvidence = canonicalizeCallerEvidence(input.evidence ?? {});
1822
+ const callerEvidenceJson = stableJson(callerEvidence);
1823
+ const keyDigest = createHash("sha256").update(idempotencyKey, "utf8").digest();
1824
+ return {
1825
+ idempotencyKey,
1826
+ operationId: uuidFromDigest(keyDigest),
1827
+ keyHash: keyDigest.toString("hex"),
1828
+ normalizedCode: String(input.code ?? "").trim().toLowerCase(),
1829
+ callerEvidence,
1830
+ callerEvidenceHash: hashText(callerEvidenceJson),
1831
+ requestedOccurredAt,
1832
+ occurredAt,
1833
+ subjectKind: input.subjectKind ?? "",
1834
+ subjectId: input.subjectId ?? "",
1835
+ maxEvidenceBytes
1836
+ };
1837
+ }
1838
+ function canonicalizeCallerEvidence(evidence) {
1839
+ try {
1840
+ const serialized = JSON.stringify(evidence);
1841
+ if (serialized === void 0) throw new TypeError("evidence is undefined");
1842
+ const parsed = JSON.parse(serialized);
1843
+ if (!isPlainObject(parsed)) throw new TypeError("evidence must be a JSON object");
1844
+ return JSON.parse(stableJson(parsed));
1845
+ } catch (error) {
1846
+ throw new ReferralClickValidationError("invalid_evidence", `Referral click evidence must be a JSON-serializable object: ${error instanceof Error ? error.message : String(error)}`);
1847
+ }
1848
+ }
1849
+ function buildClickIntent(link, request) {
1850
+ const finalEvidence = stableJson(Object.assign(/* @__PURE__ */ Object.create(null), request.callerEvidence, {
1851
+ code: link.code,
1852
+ linkId: link.id ?? "",
1853
+ targetUrl: link.targetUrl
1854
+ }));
1855
+ return {
1856
+ finalEvidence,
1857
+ intentHash: hashText(stableJson({
1858
+ tenantId: link.tenantId,
1859
+ linkId: link.id ?? "",
1860
+ code: link.code,
1861
+ targetUrl: link.targetUrl,
1862
+ referrerId: link.referrerId,
1863
+ programId: link.programId,
1864
+ subjectKind: request.subjectKind,
1865
+ subjectId: request.subjectId,
1866
+ requestedOccurredAt: request.requestedOccurredAt,
1867
+ callerEvidenceHash: request.callerEvidenceHash,
1868
+ finalEvidence
1869
+ }))
1870
+ };
1871
+ }
1872
+ function collectReplayMismatches(operation, link, touch, request) {
1873
+ const mismatches = /* @__PURE__ */ new Set();
1874
+ if (request.normalizedCode !== touch.code) mismatches.add("code");
1875
+ if (operation.linkId !== link.id || touch.linkId !== link.id || operation.touchId !== touch.id) mismatches.add("linkId");
1876
+ if (operation.tenantId !== touch.tenantId) mismatches.add("tenantId");
1877
+ if (touch.subjectKind !== request.subjectKind) mismatches.add("subjectKind");
1878
+ if (touch.subjectId !== request.subjectId) mismatches.add("subjectId");
1879
+ if (normalizePersistedInstant(operation.requestedOccurredAt) !== request.requestedOccurredAt) mismatches.add("occurredAt");
1880
+ if (operation.callerEvidenceHash !== request.callerEvidenceHash) mismatches.add("evidence");
1881
+ const persisted = touch.getEvidence();
1882
+ if (persisted.code !== touch.code) mismatches.add("code");
1883
+ if (persisted.linkId !== touch.linkId) mismatches.add("linkId");
1884
+ if (operation.intentHash !== hashPersistedClickIntent(operation, touch, persisted.targetUrl) && mismatches.size === 0) mismatches.add("intent");
1885
+ return [...mismatches];
1886
+ }
1887
+ function hashPersistedClickIntent(operation, touch, targetUrl) {
1888
+ return hashText(stableJson({
1889
+ tenantId: touch.tenantId,
1890
+ linkId: touch.linkId,
1891
+ code: touch.code,
1892
+ targetUrl,
1893
+ referrerId: touch.referrerId,
1894
+ programId: touch.programId,
1895
+ subjectKind: touch.subjectKind,
1896
+ subjectId: touch.subjectId,
1897
+ requestedOccurredAt: normalizePersistedInstant(operation.requestedOccurredAt),
1898
+ callerEvidenceHash: operation.callerEvidenceHash,
1899
+ finalEvidence: touch.evidence
1900
+ }));
1901
+ }
1902
+ function assertEvidenceBound(evidence, maxBytes) {
1903
+ const actualBytes = utf8ByteLength(evidence);
1904
+ if (actualBytes > maxBytes) throw new ReferralClickValidationError("evidence_too_large", `Referral click final evidence is ${actualBytes} UTF-8 bytes and exceeds the ${maxBytes}-byte limit`, {
1905
+ actualBytes,
1906
+ maxBytes
1907
+ });
1908
+ }
1909
+ function sameClickLinkSnapshot(left, right) {
1910
+ return left.id === right.id && left.tenantId === right.tenantId && left.code === right.code && left.targetUrl === right.targetUrl && left.referrerId === right.referrerId && left.programId === right.programId;
1911
+ }
1912
+ function hashText(value) {
1913
+ return createHash("sha256").update(value, "utf8").digest("hex");
1914
+ }
1915
+ function normalizePersistedInstant(value) {
1916
+ if (value instanceof Date) return value.toISOString();
1917
+ return typeof value === "string" ? value : String(value ?? "");
1918
+ }
1919
+ function uuidFromDigest(digest) {
1920
+ const bytes = Buffer.from(digest.subarray(0, 16));
1921
+ bytes[6] = bytes[6] & 15 | 80;
1922
+ bytes[8] = bytes[8] & 63 | 128;
1923
+ const hex = bytes.toString("hex");
1924
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1925
+ }
1926
+ function utf8ByteLength(value) {
1927
+ return Buffer.byteLength(value, "utf8");
1928
+ }
1929
+ function isWellFormedUnicode(value) {
1930
+ for (let index = 0; index < value.length; index += 1) {
1931
+ const code = value.charCodeAt(index);
1932
+ if (code >= 55296 && code <= 56319) {
1933
+ if (index + 1 >= value.length) return false;
1934
+ const next = value.charCodeAt(index + 1);
1935
+ if (next < 56320 || next > 57343) return false;
1936
+ index += 1;
1937
+ } else if (code >= 56320 && code <= 57343) return false;
1938
+ }
1939
+ return true;
1940
+ }
1941
+ function stableJson(value) {
1942
+ return JSON.stringify(sortJsonValue(value));
1943
+ }
1944
+ function sortJsonValue(value) {
1945
+ if (Array.isArray(value)) return value.map(sortJsonValue);
1946
+ if (isPlainObject(value)) {
1947
+ const sorted = /* @__PURE__ */ Object.create(null);
1948
+ for (const key of Object.keys(value).sort()) sorted[key] = sortJsonValue(value[key]);
1949
+ return sorted;
1950
+ }
1951
+ return value;
1952
+ }
1953
+ function isPlainObject(value) {
1954
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1955
+ const prototype = Object.getPrototypeOf(value);
1956
+ return prototype === Object.prototype || prototype === null;
1957
+ }
1603
1958
  //#endregion
1604
1959
  //#region src/referrals/models/ReferralProgram.ts
1605
1960
  var __defProp$2 = Object.defineProperty;
@@ -2998,6 +3353,6 @@ var ReferralQualificationService = class ReferralQualificationService {
2998
3353
  }
2999
3354
  };
3000
3355
  //#endregion
3001
- export { ATTRIBUTION_CONFLICT_BEHAVIORS as A, REFERRAL_TOUCH_KINDS as B, ReferralCollection as C, AttributionPolicyCollection as D, ReferralAgreement as E, REFERRAL_AGREEMENT_APPROVAL_MODES as F, AttributionExceptionCollection as H, REFERRAL_AGREEMENT_STATUSES as I, REFERRAL_LINK_STATUSES as L, ATTRIBUTION_EXCEPTION_STATUSES as M, ATTRIBUTION_POLICY_STATUSES as N, AttributionPolicy as O, ATTRIBUTION_RESOLUTION_MODES as P, REFERRAL_PROGRAM_STATUSES as R, assertHttpTargetUrl as S, ReferralAgreementCollection as T, AttributionException as U, REFERRER_STATUSES as V, ReferralLinkCollection as _, ReferralAgreementExecutionService as a, ReferralTouch as b, ReferrerCollection as c, ReferralTermSnapshot as d, ReferralProgramCollection as f, REFERRAL_CODE_LENGTH as g, REFERRAL_CODE_ALPHABET as h, REFERRAL_AGREEMENT_SOURCE_KIND as i, ATTRIBUTION_CREDIT_MODES as j, validateAttributionPolicyTerms as k, Referrer as l, MAX_CODE_GENERATION_ATTEMPTS as m, REFERRAL_TERMS_SNAPSHOT_KIND as n, AttributionService as o, ReferralProgram as p, ReferralCommissionService as r, QualifiedReferralOverrideError as s, ReferralQualificationService as t, ReferralTermSnapshotCollection as u, generateReferralCode as v, Referral as w, ReferralLink as x, ReferralTouchCollection as y, REFERRAL_STATUSES as z };
3356
+ export { ReferralAgreement as A, REFERRAL_AGREEMENT_STATUSES as B, ReferralTouchCollection as C, ReferralCollection as D, assertHttpTargetUrl as E, ATTRIBUTION_CREDIT_MODES as F, REFERRER_STATUSES as G, REFERRAL_PROGRAM_STATUSES as H, ATTRIBUTION_EXCEPTION_STATUSES as I, AttributionExceptionCollection as K, ATTRIBUTION_POLICY_STATUSES as L, AttributionPolicy as M, validateAttributionPolicyTerms as N, Referral as O, ATTRIBUTION_CONFLICT_BEHAVIORS as P, ATTRIBUTION_RESOLUTION_MODES as R, generateReferralCode as S, ReferralLink as T, REFERRAL_STATUSES as U, REFERRAL_LINK_STATUSES as V, REFERRAL_TOUCH_KINDS as W, REFERRAL_CODE_ALPHABET as _, ReferralAgreementExecutionService as a, ReferralClickValidationError as b, ReferrerCollection as c, ReferralTermSnapshot as d, ReferralProgramCollection as f, MAX_REFERRAL_CLICK_IDEMPOTENCY_KEY_BYTES as g, MAX_CODE_GENERATION_ATTEMPTS as h, REFERRAL_AGREEMENT_SOURCE_KIND as i, AttributionPolicyCollection as j, ReferralAgreementCollection as k, Referrer as l, DEFAULT_REFERRAL_CLICK_EVIDENCE_MAX_BYTES as m, REFERRAL_TERMS_SNAPSHOT_KIND as n, AttributionService as o, ReferralProgram as p, AttributionException as q, ReferralCommissionService as r, QualifiedReferralOverrideError as s, ReferralQualificationService as t, ReferralTermSnapshotCollection as u, REFERRAL_CODE_LENGTH as v, ReferralTouch as w, ReferralLinkCollection as x, ReferralClickReplayConflictError as y, REFERRAL_AGREEMENT_APPROVAL_MODES as z };
3002
3357
 
3003
- //# sourceMappingURL=referrals-BXqD65KR.js.map
3358
+ //# sourceMappingURL=referrals-CJUOZxpa.js.map