@happyvertical/smrt-sales 0.57.23 → 0.57.24
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/README.md +17 -0
- package/agents/commissions.md +2 -1
- package/dist/agreements.js +1 -1
- package/dist/chunks/__smrt-register__-Yzz7_2xD.js +6 -0
- package/dist/chunks/__smrt-register__-Yzz7_2xD.js.map +1 -0
- package/dist/chunks/{commissions-D5x24d4o.js → commissions-nMBMa0DT.js} +410 -344
- package/dist/chunks/commissions-nMBMa0DT.js.map +1 -0
- package/dist/chunks/{referrals-BRdTkEFt.js → referrals-BPEaXqCz.js} +2 -2
- package/dist/chunks/{referrals-BRdTkEFt.js.map → referrals-BPEaXqCz.js.map} +1 -1
- package/dist/commissions/collections/CommissionPayoutCollection.d.ts +3 -1
- package/dist/commissions/collections/CommissionPayoutCollection.d.ts.map +1 -1
- package/dist/commissions/models/CommissionPayout.d.ts +15 -1
- package/dist/commissions/models/CommissionPayout.d.ts.map +1 -1
- package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -1
- package/dist/commissions/types.d.ts +2 -0
- package/dist/commissions/types.d.ts.map +1 -1
- package/dist/commissions.js +2 -2
- package/dist/crm.js +1 -1
- package/dist/index.js +3 -3
- package/dist/manifest.json +71 -5
- package/dist/referrals.js +2 -2
- package/dist/smrt-knowledge.json +49 -5
- package/dist/svelte/types.d.ts +3 -1
- package/dist/svelte/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/chunks/__smrt-register__-BRrJGGpG.js +0 -6
- package/dist/chunks/__smrt-register__-BRrJGGpG.js.map +0 -1
- package/dist/chunks/commissions-D5x24d4o.js.map +0 -1
|
@@ -534,7 +534,7 @@ var Commission = class extends SmrtObject {
|
|
|
534
534
|
async assertDedupeKeyNotTaken() {
|
|
535
535
|
if (!this.dedupeKey) return;
|
|
536
536
|
try {
|
|
537
|
-
const res = await this.db.query(`SELECT id FROM ${this.tableName} WHERE dedupe_key = $1`, this.dedupeKey);
|
|
537
|
+
const res = await this.db.query(`SELECT CAST(id AS VARCHAR) AS id FROM ${this.tableName} WHERE dedupe_key = $1`, this.dedupeKey);
|
|
538
538
|
if ((Array.isArray(res) ? res : res.rows ?? []).find((row) => row.id !== this.id)) throw new Error(`Commission (dedupeKey '${this.dedupeKey}'): a commission with this dedupe key already exists \u2014 commissions are immutable audit rows; corrections append CommissionAdjustments.`);
|
|
539
539
|
} catch (error) {
|
|
540
540
|
if (error instanceof Error && error.message.includes("immutable")) throw error;
|
|
@@ -702,7 +702,81 @@ var CommissionCollection = class extends SmrtCollection {
|
|
|
702
702
|
}
|
|
703
703
|
};
|
|
704
704
|
//#endregion
|
|
705
|
-
//#region src/commissions/
|
|
705
|
+
//#region src/commissions/types.ts
|
|
706
|
+
var EARNER_STATUSES = [
|
|
707
|
+
"pending",
|
|
708
|
+
"active",
|
|
709
|
+
"suspended"
|
|
710
|
+
];
|
|
711
|
+
var EARNER_SOURCE_ATTRIBUTION_STATUSES = ["active", "inactive"];
|
|
712
|
+
var EARNER_KINDS = [
|
|
713
|
+
"person",
|
|
714
|
+
"organisation",
|
|
715
|
+
"treasury"
|
|
716
|
+
];
|
|
717
|
+
var PAYOUT_METHODS = [
|
|
718
|
+
"bank_transfer",
|
|
719
|
+
"check",
|
|
720
|
+
"paypal",
|
|
721
|
+
"credit",
|
|
722
|
+
"other",
|
|
723
|
+
"stripe_connect",
|
|
724
|
+
"crypto"
|
|
725
|
+
];
|
|
726
|
+
var PAYOUT_METHODS_REQUIRING_DESTINATION = ["stripe_connect", "crypto"];
|
|
727
|
+
var COMMISSION_PLAN_STATUSES = [
|
|
728
|
+
"draft",
|
|
729
|
+
"active",
|
|
730
|
+
"superseded",
|
|
731
|
+
"retired"
|
|
732
|
+
];
|
|
733
|
+
var COMMISSION_STATUSES = [
|
|
734
|
+
"pending",
|
|
735
|
+
"earned",
|
|
736
|
+
"approved",
|
|
737
|
+
"payable",
|
|
738
|
+
"paid"
|
|
739
|
+
];
|
|
740
|
+
var COMMISSION_BASES = [
|
|
741
|
+
"fixed",
|
|
742
|
+
"gross",
|
|
743
|
+
"net",
|
|
744
|
+
"margin",
|
|
745
|
+
"custom"
|
|
746
|
+
];
|
|
747
|
+
var COMMISSION_ADJUSTMENT_KINDS = [
|
|
748
|
+
"refund",
|
|
749
|
+
"credit",
|
|
750
|
+
"chargeback",
|
|
751
|
+
"dispute",
|
|
752
|
+
"correction"
|
|
753
|
+
];
|
|
754
|
+
var COMMISSION_PAYOUT_STATUSES = [
|
|
755
|
+
"pending",
|
|
756
|
+
"approved",
|
|
757
|
+
"processing",
|
|
758
|
+
"completed",
|
|
759
|
+
"failed",
|
|
760
|
+
"rejected"
|
|
761
|
+
];
|
|
762
|
+
var EARNING_EVENT_KINDS = [
|
|
763
|
+
"conversion",
|
|
764
|
+
"agreement_execution",
|
|
765
|
+
"invoice_payment",
|
|
766
|
+
"collected_revenue",
|
|
767
|
+
"recognized_margin",
|
|
768
|
+
"milestone"
|
|
769
|
+
];
|
|
770
|
+
var ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [
|
|
771
|
+
"earned",
|
|
772
|
+
"approved",
|
|
773
|
+
"payable",
|
|
774
|
+
"paid"
|
|
775
|
+
];
|
|
776
|
+
var WATERFALL_POOL_RESIDUAL_RULES = ["remainder_sink", "largest_remainder"];
|
|
777
|
+
var WATERFALL_POOL_ZERO_WEIGHT_RULES = ["remainder_sink", "equal"];
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/commissions/models/Earner.ts
|
|
706
780
|
var __defProp$4 = Object.defineProperty;
|
|
707
781
|
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
|
|
708
782
|
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
|
|
@@ -718,6 +792,197 @@ var __decorateClass$4 = (decorators, target, key, kind) => {
|
|
|
718
792
|
return result;
|
|
719
793
|
};
|
|
720
794
|
var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
795
|
+
function validatePayoutDestination(destination) {
|
|
796
|
+
const value = destination;
|
|
797
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Earner payoutDestination must be an object");
|
|
798
|
+
const requireText = (key) => {
|
|
799
|
+
if (typeof value[key] !== "string" || !value[key].trim()) throw new Error(`Earner payoutDestination.${key} is required`);
|
|
800
|
+
};
|
|
801
|
+
let allowed;
|
|
802
|
+
if (value.method === "stripe_connect") {
|
|
803
|
+
allowed = ["method", "connectedAccountId"];
|
|
804
|
+
requireText("connectedAccountId");
|
|
805
|
+
if (!/^acct_[A-Za-z0-9]+$/.test(value.connectedAccountId)) throw new Error("Earner payoutDestination.connectedAccountId must be a Stripe connected account id ('acct_…')");
|
|
806
|
+
} else if (value.method === "crypto") {
|
|
807
|
+
allowed = [
|
|
808
|
+
"method",
|
|
809
|
+
"network",
|
|
810
|
+
"asset",
|
|
811
|
+
"address",
|
|
812
|
+
"memo"
|
|
813
|
+
];
|
|
814
|
+
requireText("network");
|
|
815
|
+
requireText("asset");
|
|
816
|
+
requireText("address");
|
|
817
|
+
if (value.memo !== void 0 && typeof value.memo !== "string") throw new Error("Earner payoutDestination.memo must be a string");
|
|
818
|
+
} else throw new Error(`Earner payoutDestination.method must be one of ${PAYOUT_METHODS_REQUIRING_DESTINATION.join(", ")}`);
|
|
819
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new Error(`Earner payoutDestination has an unexpected field '${key}'`);
|
|
820
|
+
}
|
|
821
|
+
var Earner = class extends SmrtObject {
|
|
822
|
+
tenantId = null;
|
|
823
|
+
/**
|
|
824
|
+
* Party kind: `person` (default), `organisation`, or `treasury`.
|
|
825
|
+
* Organisation and treasury earners need no person Profile.
|
|
826
|
+
*/
|
|
827
|
+
kind = "person";
|
|
828
|
+
profileId = "";
|
|
829
|
+
/** Human-readable display name for portals and operator views. */
|
|
830
|
+
displayName = "";
|
|
831
|
+
/** Account lifecycle: `pending` (default) → `active` / `suspended`. */
|
|
832
|
+
status = "pending";
|
|
833
|
+
/** Preferred payout delivery method. */
|
|
834
|
+
payoutMethod = "bank_transfer";
|
|
835
|
+
/**
|
|
836
|
+
* Typed payout-rail descriptor as a JSON string (`{}` when none). Required
|
|
837
|
+
* for an active earner whose method is `stripe_connect` or `crypto`, and
|
|
838
|
+
* its `method` must match {@link payoutMethod}. Use
|
|
839
|
+
* {@link getPayoutDestination}/{@link setPayoutDestination}.
|
|
840
|
+
*/
|
|
841
|
+
payoutDestination = "{}";
|
|
842
|
+
/**
|
|
843
|
+
* Minimum unsettled balance (integer cents) before a payout batch is
|
|
844
|
+
* created. Default $50.00 = 5000 cents.
|
|
845
|
+
*/
|
|
846
|
+
payoutThresholdCents = 5e3;
|
|
847
|
+
/**
|
|
848
|
+
* Payout cadence key. Open string so applications can define their own
|
|
849
|
+
* schedules (`manual`, `monthly`, `weekly`, `net_30`, …); `manual` means
|
|
850
|
+
* an operator triggers batches explicitly.
|
|
851
|
+
*/
|
|
852
|
+
payoutScheduleKey = "manual";
|
|
853
|
+
/** ISO 4217 currency all of this earner's balances settle in. */
|
|
854
|
+
currency = "USD";
|
|
855
|
+
/**
|
|
856
|
+
* Additional metadata as a JSON string (tax info, payout-rail details,
|
|
857
|
+
* …). Use {@link getMetadata}/{@link setMetadata}.
|
|
858
|
+
*/
|
|
859
|
+
metadata = "{}";
|
|
860
|
+
constructor(options = {}) {
|
|
861
|
+
super(options);
|
|
862
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
863
|
+
if (options.kind !== void 0) this.kind = options.kind;
|
|
864
|
+
if (options.profileId !== void 0) this.profileId = options.profileId;
|
|
865
|
+
if (options.displayName !== void 0) this.displayName = options.displayName;
|
|
866
|
+
if (options.status !== void 0) this.status = options.status;
|
|
867
|
+
if (options.payoutMethod !== void 0) this.payoutMethod = options.payoutMethod;
|
|
868
|
+
if (options.payoutDestination !== void 0) this.payoutDestination = options.payoutDestination;
|
|
869
|
+
if (options.payoutThresholdCents !== void 0) this.payoutThresholdCents = options.payoutThresholdCents;
|
|
870
|
+
if (options.payoutScheduleKey !== void 0) this.payoutScheduleKey = options.payoutScheduleKey;
|
|
871
|
+
if (options.currency !== void 0) this.currency = options.currency;
|
|
872
|
+
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
873
|
+
}
|
|
874
|
+
isActive() {
|
|
875
|
+
return this.status === "active";
|
|
876
|
+
}
|
|
877
|
+
isPending() {
|
|
878
|
+
return this.status === "pending";
|
|
879
|
+
}
|
|
880
|
+
isSuspended() {
|
|
881
|
+
return this.status === "suspended";
|
|
882
|
+
}
|
|
883
|
+
/** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
|
|
884
|
+
getMetadata() {
|
|
885
|
+
if (!this.metadata) return {};
|
|
886
|
+
try {
|
|
887
|
+
const parsed = JSON.parse(this.metadata);
|
|
888
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
889
|
+
} catch {
|
|
890
|
+
return {};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/** Serialize and store {@link metadata}. */
|
|
894
|
+
setMetadata(data) {
|
|
895
|
+
this.metadata = JSON.stringify(data ?? {});
|
|
896
|
+
}
|
|
897
|
+
isOrganisation() {
|
|
898
|
+
return this.kind === "organisation";
|
|
899
|
+
}
|
|
900
|
+
isTreasury() {
|
|
901
|
+
return this.kind === "treasury";
|
|
902
|
+
}
|
|
903
|
+
/** Parse {@link payoutDestination}; `null` when none or unparseable. */
|
|
904
|
+
getPayoutDestination() {
|
|
905
|
+
try {
|
|
906
|
+
const parsed = JSON.parse(this.payoutDestination || "{}");
|
|
907
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0 ? parsed : null;
|
|
908
|
+
} catch {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Validate and store a payout destination, and set {@link payoutMethod}
|
|
914
|
+
* to its method. `null` clears it (the method is left unchanged).
|
|
915
|
+
*/
|
|
916
|
+
setPayoutDestination(destination) {
|
|
917
|
+
if (destination === null) {
|
|
918
|
+
this.payoutDestination = "{}";
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
validatePayoutDestination(destination);
|
|
922
|
+
this.payoutDestination = JSON.stringify(destination);
|
|
923
|
+
this.payoutMethod = destination.method;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Save with field guards: `kind` and `payoutMethod` must be known values,
|
|
927
|
+
* a stored destination must be valid and match `payoutMethod`, and an
|
|
928
|
+
* active Stripe Connect / crypto earner must have a destination.
|
|
929
|
+
*/
|
|
930
|
+
async save() {
|
|
931
|
+
this.assertPayoutTerms();
|
|
932
|
+
return await super.save();
|
|
933
|
+
}
|
|
934
|
+
assertPayoutTerms() {
|
|
935
|
+
const label = `Earner ${this.id ?? "<new>"}`;
|
|
936
|
+
if (!EARNER_KINDS.includes(this.kind)) throw new Error(`${label}: unknown kind '${this.kind}'`);
|
|
937
|
+
if (!PAYOUT_METHODS.includes(this.payoutMethod)) throw new Error(`${label}: unknown payoutMethod '${this.payoutMethod}'`);
|
|
938
|
+
let parsed;
|
|
939
|
+
try {
|
|
940
|
+
parsed = JSON.parse(this.payoutDestination || "{}");
|
|
941
|
+
} catch {
|
|
942
|
+
throw new Error(`${label}: payoutDestination must be valid JSON`);
|
|
943
|
+
}
|
|
944
|
+
const hasDestination = !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0;
|
|
945
|
+
if (hasDestination) {
|
|
946
|
+
validatePayoutDestination(parsed);
|
|
947
|
+
if (parsed.method !== this.payoutMethod) throw new Error(`${label}: payoutDestination method '${parsed.method}' does not match payoutMethod '${this.payoutMethod}'`);
|
|
948
|
+
} else if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label}: payoutDestination must be a JSON object`);
|
|
949
|
+
if (this.status === "active" && !hasDestination && PAYOUT_METHODS_REQUIRING_DESTINATION.includes(this.payoutMethod)) throw new Error(`${label}: an active ${this.payoutMethod} earner requires a payoutDestination`);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
__publicField$4(Earner, "__smrtPackage__", "@happyvertical/smrt-sales");
|
|
953
|
+
__decorateClass$4([tenantId({ nullable: true })], Earner.prototype, "tenantId", 2);
|
|
954
|
+
__decorateClass$4([crossPackageRef("@happyvertical/smrt-profiles:Profile")], Earner.prototype, "profileId", 2);
|
|
955
|
+
Earner = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
|
|
956
|
+
api: { include: [
|
|
957
|
+
"list",
|
|
958
|
+
"get",
|
|
959
|
+
"create",
|
|
960
|
+
"update"
|
|
961
|
+
] },
|
|
962
|
+
mcp: { include: [
|
|
963
|
+
"list",
|
|
964
|
+
"get",
|
|
965
|
+
"create"
|
|
966
|
+
] },
|
|
967
|
+
cli: { skipApiCheck: true }
|
|
968
|
+
})], Earner);
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region src/commissions/models/CommissionPayout.ts
|
|
971
|
+
var __defProp$3 = Object.defineProperty;
|
|
972
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
973
|
+
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, {
|
|
974
|
+
enumerable: true,
|
|
975
|
+
configurable: true,
|
|
976
|
+
writable: true,
|
|
977
|
+
value
|
|
978
|
+
}) : obj[key] = value;
|
|
979
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
980
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
981
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
982
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
983
|
+
return result;
|
|
984
|
+
};
|
|
985
|
+
var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
721
986
|
var PAYOUT_STATUS_TRANSITIONS = {
|
|
722
987
|
pending: ["approved", "rejected"],
|
|
723
988
|
approved: [
|
|
@@ -755,6 +1020,13 @@ var CommissionPayout = class extends SmrtObject {
|
|
|
755
1020
|
/** Delivery method for this batch (defaulted from the Earner). */
|
|
756
1021
|
payoutMethod = "bank_transfer";
|
|
757
1022
|
/**
|
|
1023
|
+
* Routing descriptor captured at batch creation, never the live Earner's
|
|
1024
|
+
* preference. Routing is immutable after the first save, including pending
|
|
1025
|
+
* and failed/reset batches; reject and create a new batch to change it.
|
|
1026
|
+
* Legacy/manual methods have no typed descriptor (`{}`).
|
|
1027
|
+
*/
|
|
1028
|
+
payoutDestination = "{}";
|
|
1029
|
+
/**
|
|
758
1030
|
* Lifecycle status — see the class doc. Mutate via {@link approve} /
|
|
759
1031
|
* {@link markProcessing} / {@link complete} / {@link fail} /
|
|
760
1032
|
* {@link resetFromFailed}.
|
|
@@ -802,6 +1074,7 @@ var CommissionPayout = class extends SmrtObject {
|
|
|
802
1074
|
if (options.totalAmountCents !== void 0) this.totalAmountCents = options.totalAmountCents;
|
|
803
1075
|
if (options.currency !== void 0) this.currency = options.currency;
|
|
804
1076
|
if (options.payoutMethod !== void 0) this.payoutMethod = options.payoutMethod;
|
|
1077
|
+
if (options.payoutDestination !== void 0) this.payoutDestination = options.payoutDestination;
|
|
805
1078
|
if (options.status !== void 0) this.status = options.status;
|
|
806
1079
|
if (options.paymentReference !== void 0) this.paymentReference = options.paymentReference;
|
|
807
1080
|
if (options.providerRef !== void 0) this.providerRef = options.providerRef;
|
|
@@ -847,6 +1120,7 @@ var CommissionPayout = class extends SmrtObject {
|
|
|
847
1120
|
approve() {
|
|
848
1121
|
if (this.status !== "pending") throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot approve from status '${this.status}'`);
|
|
849
1122
|
if (this.totalAmountCents <= 0) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot approve a batch with non-positive total (${this.totalAmountCents} cents)`);
|
|
1123
|
+
this.validateRouting(true);
|
|
850
1124
|
this.status = "approved";
|
|
851
1125
|
}
|
|
852
1126
|
/** `approved → processing`. Does NOT save — the caller saves. */
|
|
@@ -908,6 +1182,16 @@ ${memo}` : memo;
|
|
|
908
1182
|
this.paymentReference = "";
|
|
909
1183
|
this.paidAt = null;
|
|
910
1184
|
}
|
|
1185
|
+
/** Read the batch's validated routing snapshot, never a live Earner. */
|
|
1186
|
+
getPayoutDestination() {
|
|
1187
|
+
return this.validateRouting(false);
|
|
1188
|
+
}
|
|
1189
|
+
/** Set a descriptor before first persistence; save enforces routing freeze. */
|
|
1190
|
+
setPayoutDestination(destination) {
|
|
1191
|
+
if (destination) validatePayoutDestination(destination);
|
|
1192
|
+
this.payoutDestination = JSON.stringify(destination ?? {});
|
|
1193
|
+
if (destination) this.payoutMethod = destination.method;
|
|
1194
|
+
}
|
|
911
1195
|
/** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
|
|
912
1196
|
getMetadata() {
|
|
913
1197
|
if (!this.metadata) return {};
|
|
@@ -937,13 +1221,51 @@ ${memo}` : memo;
|
|
|
937
1221
|
*/
|
|
938
1222
|
async save() {
|
|
939
1223
|
this.validateTotals();
|
|
1224
|
+
await this.assertRoutingUnchanged();
|
|
940
1225
|
const prior = await this.resolvePriorStatus();
|
|
1226
|
+
this.validateRouting(prior !== this.status && [
|
|
1227
|
+
"approved",
|
|
1228
|
+
"processing",
|
|
1229
|
+
"completed"
|
|
1230
|
+
].includes(this.status));
|
|
941
1231
|
this.assertStatusTransition(prior);
|
|
942
1232
|
if (this.status === "completed" && !this.paymentReference) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: a completed payout requires a paymentReference (use complete()).`);
|
|
943
1233
|
const result = await super.save();
|
|
944
1234
|
loadedPayoutStatus.set(this, this.status);
|
|
945
1235
|
return result;
|
|
946
1236
|
}
|
|
1237
|
+
validateRouting(requireDestination) {
|
|
1238
|
+
const label = `CommissionPayout ${this.id ?? "<new>"}`;
|
|
1239
|
+
if (!PAYOUT_METHODS.includes(this.payoutMethod)) throw new Error(`${label}: unknown payoutMethod '${this.payoutMethod}'`);
|
|
1240
|
+
let parsed;
|
|
1241
|
+
try {
|
|
1242
|
+
parsed = JSON.parse(this.payoutDestination);
|
|
1243
|
+
} catch {
|
|
1244
|
+
throw new Error(`${label}: payoutDestination must be valid JSON`);
|
|
1245
|
+
}
|
|
1246
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label}: payoutDestination must be a JSON object`);
|
|
1247
|
+
if (Object.keys(parsed).length > 0) {
|
|
1248
|
+
validatePayoutDestination(parsed);
|
|
1249
|
+
const destination = parsed;
|
|
1250
|
+
if (destination.method !== this.payoutMethod) throw new Error(`${label}: payoutDestination method does not match payoutMethod`);
|
|
1251
|
+
return destination;
|
|
1252
|
+
}
|
|
1253
|
+
if (requireDestination && PAYOUT_METHODS_REQUIRING_DESTINATION.includes(this.payoutMethod)) throw new Error(`${label}: ${this.payoutMethod} requires a payoutDestination snapshot; reject and recreate the batch`);
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
/** Authoritative read also closes blind-id and natural-key upsert doors. */
|
|
1257
|
+
async assertRoutingUnchanged() {
|
|
1258
|
+
const { rows } = await this.db.query(`SELECT CAST(id AS VARCHAR) AS id, payout_method, payout_destination FROM ${this.tableName} WHERE id = ? OR idempotency_key = ?`, this.id ?? null, this.idempotencyKey || null);
|
|
1259
|
+
for (const row of rows) {
|
|
1260
|
+
if (row.id !== this.id) throw new Error("CommissionPayout: existing batch routing is immutable; reuse its idempotency key through the payout service");
|
|
1261
|
+
const priorDestination = String(row.payout_destination ?? "{}");
|
|
1262
|
+
const canonical = (raw) => {
|
|
1263
|
+
const parsed = JSON.parse(raw);
|
|
1264
|
+
return JSON.stringify(Object.entries(parsed).sort(([a], [b]) => a.localeCompare(b)));
|
|
1265
|
+
};
|
|
1266
|
+
if (row.payout_method !== this.payoutMethod || canonical(priorDestination) !== canonical(this.payoutDestination)) throw new Error("CommissionPayout: routing is immutable; reject and recreate the batch");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
947
1269
|
/** Throws when the totals invariant doesn't hold. */
|
|
948
1270
|
validateTotals() {
|
|
949
1271
|
for (const [name, value] of [
|
|
@@ -976,13 +1298,13 @@ ${memo}` : memo;
|
|
|
976
1298
|
return null;
|
|
977
1299
|
}
|
|
978
1300
|
};
|
|
979
|
-
__publicField$
|
|
980
|
-
__decorateClass$
|
|
981
|
-
__decorateClass$
|
|
982
|
-
__decorateClass$
|
|
983
|
-
__decorateClass$
|
|
984
|
-
__decorateClass$
|
|
985
|
-
CommissionPayout = __decorateClass$
|
|
1301
|
+
__publicField$3(CommissionPayout, "__smrtPackage__", "@happyvertical/smrt-sales");
|
|
1302
|
+
__decorateClass$3([tenantId({ nullable: true })], CommissionPayout.prototype, "tenantId", 2);
|
|
1303
|
+
__decorateClass$3([foreignKey("Earner", { required: true })], CommissionPayout.prototype, "earnerId", 2);
|
|
1304
|
+
__decorateClass$3([crossPackageRef("@happyvertical/smrt-commerce:Invoice")], CommissionPayout.prototype, "invoiceId", 2);
|
|
1305
|
+
__decorateClass$3([field({ required: true })], CommissionPayout.prototype, "idempotencyKey", 2);
|
|
1306
|
+
__decorateClass$3([field({ indexed: true })], CommissionPayout.prototype, "sourceId", 2);
|
|
1307
|
+
CommissionPayout = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
|
|
986
1308
|
conflictColumns: ["idempotency_key"],
|
|
987
1309
|
api: { include: ["list", "get"] },
|
|
988
1310
|
mcp: { include: ["list", "get"] },
|
|
@@ -992,135 +1314,68 @@ CommissionPayout = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
|
|
|
992
1314
|
//#region src/commissions/collections/CommissionPayoutCollection.ts
|
|
993
1315
|
var CommissionPayoutCollection = class extends SmrtCollection {
|
|
994
1316
|
static _itemClass = CommissionPayout;
|
|
995
|
-
/**
|
|
996
|
-
async
|
|
997
|
-
return
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
/** Payout batches by status, newest first. */
|
|
1003
|
-
async findByStatus(status) {
|
|
1004
|
-
return await this.list({
|
|
1005
|
-
where: { status },
|
|
1006
|
-
orderBy: "created_at DESC"
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
/** Look up a payout by its idempotency natural key. */
|
|
1010
|
-
async findByIdempotencyKey(idempotencyKey) {
|
|
1011
|
-
if (!idempotencyKey) return null;
|
|
1012
|
-
return (await this.list({
|
|
1013
|
-
where: { idempotencyKey },
|
|
1014
|
-
limit: 1
|
|
1015
|
-
}))[0] ?? null;
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
|
-
* One page of payouts carrying the derived single-source stamp for
|
|
1019
|
-
* `(sourceKind, sourceId)`, newest first with a deterministic id
|
|
1020
|
-
* tiebreak. This is the RAW indexed page — stamped rows only, membership
|
|
1021
|
-
* unverified. Consumers want
|
|
1022
|
-
* `CommissionPayoutService.getSourcePayoutHistory`, which re-verifies
|
|
1023
|
-
* each page's membership and fails closed on rows the stamp alone cannot
|
|
1024
|
-
* prove.
|
|
1025
|
-
*/
|
|
1026
|
-
async findBySource(sourceKind, sourceId, page) {
|
|
1027
|
-
if (!sourceKind || !sourceId) return [];
|
|
1028
|
-
return await this.list({
|
|
1029
|
-
where: {
|
|
1030
|
-
sourceKind,
|
|
1031
|
-
sourceId
|
|
1032
|
-
},
|
|
1033
|
-
orderBy: ["created_at DESC", "id DESC"],
|
|
1034
|
-
limit: page.limit,
|
|
1035
|
-
offset: page.offset
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
/**
|
|
1039
|
-
* Σ totalAmountCents of COMPLETED payouts for an earner+currency —
|
|
1040
|
-
* lifetime settled earnings (integer cents).
|
|
1041
|
-
*/
|
|
1042
|
-
async sumPaidByEarner(earnerId, currency) {
|
|
1043
|
-
return (await this.list({ where: {
|
|
1044
|
-
earnerId,
|
|
1045
|
-
currency,
|
|
1046
|
-
status: "completed"
|
|
1047
|
-
} })).reduce((sum, p) => sum + p.totalAmountCents, 0);
|
|
1317
|
+
/** Persist the first routing snapshot atomically; a duplicate key must not upsert it. */
|
|
1318
|
+
async create(options, context) {
|
|
1319
|
+
return super.create({
|
|
1320
|
+
...options,
|
|
1321
|
+
_insertOnly: true
|
|
1322
|
+
}, context);
|
|
1048
1323
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
"completed",
|
|
1105
|
-
"failed",
|
|
1106
|
-
"rejected"
|
|
1107
|
-
];
|
|
1108
|
-
var EARNING_EVENT_KINDS = [
|
|
1109
|
-
"conversion",
|
|
1110
|
-
"agreement_execution",
|
|
1111
|
-
"invoice_payment",
|
|
1112
|
-
"collected_revenue",
|
|
1113
|
-
"recognized_margin",
|
|
1114
|
-
"milestone"
|
|
1115
|
-
];
|
|
1116
|
-
var ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [
|
|
1117
|
-
"earned",
|
|
1118
|
-
"approved",
|
|
1119
|
-
"payable",
|
|
1120
|
-
"paid"
|
|
1121
|
-
];
|
|
1122
|
-
var WATERFALL_POOL_RESIDUAL_RULES = ["remainder_sink", "largest_remainder"];
|
|
1123
|
-
var WATERFALL_POOL_ZERO_WEIGHT_RULES = ["remainder_sink", "equal"];
|
|
1324
|
+
/** All payout batches for an earner, newest first. */
|
|
1325
|
+
async findByEarner(earnerId) {
|
|
1326
|
+
return await this.list({
|
|
1327
|
+
where: { earnerId },
|
|
1328
|
+
orderBy: "created_at DESC"
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
/** Payout batches by status, newest first. */
|
|
1332
|
+
async findByStatus(status) {
|
|
1333
|
+
return await this.list({
|
|
1334
|
+
where: { status },
|
|
1335
|
+
orderBy: "created_at DESC"
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
/** Look up a payout by its idempotency natural key. */
|
|
1339
|
+
async findByIdempotencyKey(idempotencyKey) {
|
|
1340
|
+
if (!idempotencyKey) return null;
|
|
1341
|
+
return (await this.list({
|
|
1342
|
+
where: { idempotencyKey },
|
|
1343
|
+
limit: 1
|
|
1344
|
+
}))[0] ?? null;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* One page of payouts carrying the derived single-source stamp for
|
|
1348
|
+
* `(sourceKind, sourceId)`, newest first with a deterministic id
|
|
1349
|
+
* tiebreak. This is the RAW indexed page — stamped rows only, membership
|
|
1350
|
+
* unverified. Consumers want
|
|
1351
|
+
* `CommissionPayoutService.getSourcePayoutHistory`, which re-verifies
|
|
1352
|
+
* each page's membership and fails closed on rows the stamp alone cannot
|
|
1353
|
+
* prove.
|
|
1354
|
+
*/
|
|
1355
|
+
async findBySource(sourceKind, sourceId, page) {
|
|
1356
|
+
if (!sourceKind || !sourceId) return [];
|
|
1357
|
+
return await this.list({
|
|
1358
|
+
where: {
|
|
1359
|
+
sourceKind,
|
|
1360
|
+
sourceId
|
|
1361
|
+
},
|
|
1362
|
+
orderBy: ["created_at DESC", "id DESC"],
|
|
1363
|
+
limit: page.limit,
|
|
1364
|
+
offset: page.offset
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Σ totalAmountCents of COMPLETED payouts for an earner+currency —
|
|
1369
|
+
* lifetime settled earnings (integer cents).
|
|
1370
|
+
*/
|
|
1371
|
+
async sumPaidByEarner(earnerId, currency) {
|
|
1372
|
+
return (await this.list({ where: {
|
|
1373
|
+
earnerId,
|
|
1374
|
+
currency,
|
|
1375
|
+
status: "completed"
|
|
1376
|
+
} })).reduce((sum, p) => sum + p.totalAmountCents, 0);
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1124
1379
|
//#endregion
|
|
1125
1380
|
//#region src/commissions/services/WaterfallCalculationService.ts
|
|
1126
1381
|
var WaterfallRecipientResolverRegistry = class {
|
|
@@ -1616,21 +1871,21 @@ function assertWholeNonNegative(value, label) {
|
|
|
1616
1871
|
}
|
|
1617
1872
|
//#endregion
|
|
1618
1873
|
//#region src/commissions/models/CommissionPlan.ts
|
|
1619
|
-
var __defProp$
|
|
1620
|
-
var __getOwnPropDesc$
|
|
1621
|
-
var __defNormalProp$
|
|
1874
|
+
var __defProp$2 = Object.defineProperty;
|
|
1875
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
1876
|
+
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
|
|
1622
1877
|
enumerable: true,
|
|
1623
1878
|
configurable: true,
|
|
1624
1879
|
writable: true,
|
|
1625
1880
|
value
|
|
1626
1881
|
}) : obj[key] = value;
|
|
1627
|
-
var __decorateClass$
|
|
1628
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$
|
|
1882
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
1883
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
1629
1884
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1630
|
-
if (kind && result) __defProp$
|
|
1885
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
1631
1886
|
return result;
|
|
1632
1887
|
};
|
|
1633
|
-
var __publicField$
|
|
1888
|
+
var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1634
1889
|
var PLAN_STATUS_TRANSITIONS = {
|
|
1635
1890
|
draft: ["active", "retired"],
|
|
1636
1891
|
active: ["superseded", "retired"],
|
|
@@ -1971,10 +2226,10 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1971
2226
|
return null;
|
|
1972
2227
|
}
|
|
1973
2228
|
};
|
|
1974
|
-
__publicField$
|
|
1975
|
-
__decorateClass$
|
|
1976
|
-
__decorateClass$
|
|
1977
|
-
CommissionPlan = __decorateClass$
|
|
2229
|
+
__publicField$2(CommissionPlan, "__smrtPackage__", "@happyvertical/smrt-sales");
|
|
2230
|
+
__decorateClass$2([tenantId({ nullable: true })], CommissionPlan.prototype, "tenantId", 2);
|
|
2231
|
+
__decorateClass$2([field({ required: true })], CommissionPlan.prototype, "planKey", 2);
|
|
2232
|
+
CommissionPlan = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
|
|
1978
2233
|
conflictColumns: [
|
|
1979
2234
|
"tenant_id",
|
|
1980
2235
|
"plan_key",
|
|
@@ -2086,197 +2341,6 @@ var CommissionPlanCollection = class extends SmrtCollection {
|
|
|
2086
2341
|
}
|
|
2087
2342
|
};
|
|
2088
2343
|
//#endregion
|
|
2089
|
-
//#region src/commissions/models/Earner.ts
|
|
2090
|
-
var __defProp$2 = Object.defineProperty;
|
|
2091
|
-
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
2092
|
-
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
|
|
2093
|
-
enumerable: true,
|
|
2094
|
-
configurable: true,
|
|
2095
|
-
writable: true,
|
|
2096
|
-
value
|
|
2097
|
-
}) : obj[key] = value;
|
|
2098
|
-
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
2099
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
2100
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
2101
|
-
if (kind && result) __defProp$2(target, key, result);
|
|
2102
|
-
return result;
|
|
2103
|
-
};
|
|
2104
|
-
var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
2105
|
-
function validatePayoutDestination(destination) {
|
|
2106
|
-
const value = destination;
|
|
2107
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Earner payoutDestination must be an object");
|
|
2108
|
-
const requireText = (key) => {
|
|
2109
|
-
if (typeof value[key] !== "string" || !value[key].trim()) throw new Error(`Earner payoutDestination.${key} is required`);
|
|
2110
|
-
};
|
|
2111
|
-
let allowed;
|
|
2112
|
-
if (value.method === "stripe_connect") {
|
|
2113
|
-
allowed = ["method", "connectedAccountId"];
|
|
2114
|
-
requireText("connectedAccountId");
|
|
2115
|
-
if (!/^acct_[A-Za-z0-9]+$/.test(value.connectedAccountId)) throw new Error("Earner payoutDestination.connectedAccountId must be a Stripe connected account id ('acct_…')");
|
|
2116
|
-
} else if (value.method === "crypto") {
|
|
2117
|
-
allowed = [
|
|
2118
|
-
"method",
|
|
2119
|
-
"network",
|
|
2120
|
-
"asset",
|
|
2121
|
-
"address",
|
|
2122
|
-
"memo"
|
|
2123
|
-
];
|
|
2124
|
-
requireText("network");
|
|
2125
|
-
requireText("asset");
|
|
2126
|
-
requireText("address");
|
|
2127
|
-
if (value.memo !== void 0 && typeof value.memo !== "string") throw new Error("Earner payoutDestination.memo must be a string");
|
|
2128
|
-
} else throw new Error(`Earner payoutDestination.method must be one of ${PAYOUT_METHODS_REQUIRING_DESTINATION.join(", ")}`);
|
|
2129
|
-
for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new Error(`Earner payoutDestination has an unexpected field '${key}'`);
|
|
2130
|
-
}
|
|
2131
|
-
var Earner = class extends SmrtObject {
|
|
2132
|
-
tenantId = null;
|
|
2133
|
-
/**
|
|
2134
|
-
* Party kind: `person` (default), `organisation`, or `treasury`.
|
|
2135
|
-
* Organisation and treasury earners need no person Profile.
|
|
2136
|
-
*/
|
|
2137
|
-
kind = "person";
|
|
2138
|
-
profileId = "";
|
|
2139
|
-
/** Human-readable display name for portals and operator views. */
|
|
2140
|
-
displayName = "";
|
|
2141
|
-
/** Account lifecycle: `pending` (default) → `active` / `suspended`. */
|
|
2142
|
-
status = "pending";
|
|
2143
|
-
/** Preferred payout delivery method. */
|
|
2144
|
-
payoutMethod = "bank_transfer";
|
|
2145
|
-
/**
|
|
2146
|
-
* Typed payout-rail descriptor as a JSON string (`{}` when none). Required
|
|
2147
|
-
* for an active earner whose method is `stripe_connect` or `crypto`, and
|
|
2148
|
-
* its `method` must match {@link payoutMethod}. Use
|
|
2149
|
-
* {@link getPayoutDestination}/{@link setPayoutDestination}.
|
|
2150
|
-
*/
|
|
2151
|
-
payoutDestination = "{}";
|
|
2152
|
-
/**
|
|
2153
|
-
* Minimum unsettled balance (integer cents) before a payout batch is
|
|
2154
|
-
* created. Default $50.00 = 5000 cents.
|
|
2155
|
-
*/
|
|
2156
|
-
payoutThresholdCents = 5e3;
|
|
2157
|
-
/**
|
|
2158
|
-
* Payout cadence key. Open string so applications can define their own
|
|
2159
|
-
* schedules (`manual`, `monthly`, `weekly`, `net_30`, …); `manual` means
|
|
2160
|
-
* an operator triggers batches explicitly.
|
|
2161
|
-
*/
|
|
2162
|
-
payoutScheduleKey = "manual";
|
|
2163
|
-
/** ISO 4217 currency all of this earner's balances settle in. */
|
|
2164
|
-
currency = "USD";
|
|
2165
|
-
/**
|
|
2166
|
-
* Additional metadata as a JSON string (tax info, payout-rail details,
|
|
2167
|
-
* …). Use {@link getMetadata}/{@link setMetadata}.
|
|
2168
|
-
*/
|
|
2169
|
-
metadata = "{}";
|
|
2170
|
-
constructor(options = {}) {
|
|
2171
|
-
super(options);
|
|
2172
|
-
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
2173
|
-
if (options.kind !== void 0) this.kind = options.kind;
|
|
2174
|
-
if (options.profileId !== void 0) this.profileId = options.profileId;
|
|
2175
|
-
if (options.displayName !== void 0) this.displayName = options.displayName;
|
|
2176
|
-
if (options.status !== void 0) this.status = options.status;
|
|
2177
|
-
if (options.payoutMethod !== void 0) this.payoutMethod = options.payoutMethod;
|
|
2178
|
-
if (options.payoutDestination !== void 0) this.payoutDestination = options.payoutDestination;
|
|
2179
|
-
if (options.payoutThresholdCents !== void 0) this.payoutThresholdCents = options.payoutThresholdCents;
|
|
2180
|
-
if (options.payoutScheduleKey !== void 0) this.payoutScheduleKey = options.payoutScheduleKey;
|
|
2181
|
-
if (options.currency !== void 0) this.currency = options.currency;
|
|
2182
|
-
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
2183
|
-
}
|
|
2184
|
-
isActive() {
|
|
2185
|
-
return this.status === "active";
|
|
2186
|
-
}
|
|
2187
|
-
isPending() {
|
|
2188
|
-
return this.status === "pending";
|
|
2189
|
-
}
|
|
2190
|
-
isSuspended() {
|
|
2191
|
-
return this.status === "suspended";
|
|
2192
|
-
}
|
|
2193
|
-
/** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
|
|
2194
|
-
getMetadata() {
|
|
2195
|
-
if (!this.metadata) return {};
|
|
2196
|
-
try {
|
|
2197
|
-
const parsed = JSON.parse(this.metadata);
|
|
2198
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2199
|
-
} catch {
|
|
2200
|
-
return {};
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
/** Serialize and store {@link metadata}. */
|
|
2204
|
-
setMetadata(data) {
|
|
2205
|
-
this.metadata = JSON.stringify(data ?? {});
|
|
2206
|
-
}
|
|
2207
|
-
isOrganisation() {
|
|
2208
|
-
return this.kind === "organisation";
|
|
2209
|
-
}
|
|
2210
|
-
isTreasury() {
|
|
2211
|
-
return this.kind === "treasury";
|
|
2212
|
-
}
|
|
2213
|
-
/** Parse {@link payoutDestination}; `null` when none or unparseable. */
|
|
2214
|
-
getPayoutDestination() {
|
|
2215
|
-
try {
|
|
2216
|
-
const parsed = JSON.parse(this.payoutDestination || "{}");
|
|
2217
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0 ? parsed : null;
|
|
2218
|
-
} catch {
|
|
2219
|
-
return null;
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* Validate and store a payout destination, and set {@link payoutMethod}
|
|
2224
|
-
* to its method. `null` clears it (the method is left unchanged).
|
|
2225
|
-
*/
|
|
2226
|
-
setPayoutDestination(destination) {
|
|
2227
|
-
if (destination === null) {
|
|
2228
|
-
this.payoutDestination = "{}";
|
|
2229
|
-
return;
|
|
2230
|
-
}
|
|
2231
|
-
validatePayoutDestination(destination);
|
|
2232
|
-
this.payoutDestination = JSON.stringify(destination);
|
|
2233
|
-
this.payoutMethod = destination.method;
|
|
2234
|
-
}
|
|
2235
|
-
/**
|
|
2236
|
-
* Save with field guards: `kind` and `payoutMethod` must be known values,
|
|
2237
|
-
* a stored destination must be valid and match `payoutMethod`, and an
|
|
2238
|
-
* active Stripe Connect / crypto earner must have a destination.
|
|
2239
|
-
*/
|
|
2240
|
-
async save() {
|
|
2241
|
-
this.assertPayoutTerms();
|
|
2242
|
-
return await super.save();
|
|
2243
|
-
}
|
|
2244
|
-
assertPayoutTerms() {
|
|
2245
|
-
const label = `Earner ${this.id ?? "<new>"}`;
|
|
2246
|
-
if (!EARNER_KINDS.includes(this.kind)) throw new Error(`${label}: unknown kind '${this.kind}'`);
|
|
2247
|
-
if (!PAYOUT_METHODS.includes(this.payoutMethod)) throw new Error(`${label}: unknown payoutMethod '${this.payoutMethod}'`);
|
|
2248
|
-
let parsed;
|
|
2249
|
-
try {
|
|
2250
|
-
parsed = JSON.parse(this.payoutDestination || "{}");
|
|
2251
|
-
} catch {
|
|
2252
|
-
throw new Error(`${label}: payoutDestination must be valid JSON`);
|
|
2253
|
-
}
|
|
2254
|
-
const hasDestination = !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0;
|
|
2255
|
-
if (hasDestination) {
|
|
2256
|
-
validatePayoutDestination(parsed);
|
|
2257
|
-
if (parsed.method !== this.payoutMethod) throw new Error(`${label}: payoutDestination method '${parsed.method}' does not match payoutMethod '${this.payoutMethod}'`);
|
|
2258
|
-
} else if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label}: payoutDestination must be a JSON object`);
|
|
2259
|
-
if (this.status === "active" && !hasDestination && PAYOUT_METHODS_REQUIRING_DESTINATION.includes(this.payoutMethod)) throw new Error(`${label}: an active ${this.payoutMethod} earner requires a payoutDestination`);
|
|
2260
|
-
}
|
|
2261
|
-
};
|
|
2262
|
-
__publicField$2(Earner, "__smrtPackage__", "@happyvertical/smrt-sales");
|
|
2263
|
-
__decorateClass$2([tenantId({ nullable: true })], Earner.prototype, "tenantId", 2);
|
|
2264
|
-
__decorateClass$2([crossPackageRef("@happyvertical/smrt-profiles:Profile")], Earner.prototype, "profileId", 2);
|
|
2265
|
-
Earner = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
|
|
2266
|
-
api: { include: [
|
|
2267
|
-
"list",
|
|
2268
|
-
"get",
|
|
2269
|
-
"create",
|
|
2270
|
-
"update"
|
|
2271
|
-
] },
|
|
2272
|
-
mcp: { include: [
|
|
2273
|
-
"list",
|
|
2274
|
-
"get",
|
|
2275
|
-
"create"
|
|
2276
|
-
] },
|
|
2277
|
-
cli: { skipApiCheck: true }
|
|
2278
|
-
})], Earner);
|
|
2279
|
-
//#endregion
|
|
2280
2344
|
//#region src/commissions/collections/EarnerCollection.ts
|
|
2281
2345
|
var EarnerCollection = class extends SmrtCollection {
|
|
2282
2346
|
static _itemClass = Earner;
|
|
@@ -3322,20 +3386,20 @@ var CommissionPayoutService = class CommissionPayoutService {
|
|
|
3322
3386
|
settledCommissionIds: [],
|
|
3323
3387
|
settledAdjustmentIds: []
|
|
3324
3388
|
};
|
|
3325
|
-
const
|
|
3389
|
+
const payout = await this.deps.payouts.create({
|
|
3326
3390
|
tenantId: earner.tenantId,
|
|
3327
3391
|
earnerId: input.earnerId,
|
|
3328
3392
|
currency: input.currency,
|
|
3329
3393
|
periodStart: input.periodStart ?? null,
|
|
3330
3394
|
periodEnd: input.periodEnd ?? null,
|
|
3331
3395
|
payoutMethod,
|
|
3396
|
+
payoutDestination: JSON.stringify(earner.getPayoutDestination()?.method === payoutMethod ? earner.getPayoutDestination() : {}),
|
|
3332
3397
|
status: "pending",
|
|
3333
3398
|
commissionTotalCents,
|
|
3334
3399
|
adjustmentTotalCents,
|
|
3335
3400
|
totalAmountCents: netTotalCents,
|
|
3336
3401
|
idempotencyKey
|
|
3337
3402
|
});
|
|
3338
|
-
const payout = await this.deps.payouts.findByIdempotencyKey(idempotencyKey) ?? minted;
|
|
3339
3403
|
return {
|
|
3340
3404
|
...await this.claimAndReconcile(payout, input),
|
|
3341
3405
|
created: true
|
|
@@ -3429,6 +3493,8 @@ var CommissionPayoutService = class CommissionPayoutService {
|
|
|
3429
3493
|
const payout = await this.requirePayout(payoutId);
|
|
3430
3494
|
if (!payout.isProcessing()) throw new Error(`CommissionPayout ${payout.id ?? "<new>"}: cannot complete from status '${payout.status}'`);
|
|
3431
3495
|
if (!paymentReference) throw new Error(`CommissionPayout ${payout.id ?? "<new>"}: complete() requires a paymentReference`);
|
|
3496
|
+
const destination = payout.getPayoutDestination();
|
|
3497
|
+
if (PAYOUT_METHODS_REQUIRING_DESTINATION.includes(payout.payoutMethod) && !destination) throw new Error(`CommissionPayout ${payout.id ?? "<new>"}: ${payout.payoutMethod} requires a payoutDestination snapshot; reject and recreate the batch`);
|
|
3432
3498
|
const members = await this.deps.commissions.findByPayout(payoutId);
|
|
3433
3499
|
for (const commission of members) if (commission.isPayable()) {
|
|
3434
3500
|
commission.markPaid(now);
|
|
@@ -3817,7 +3883,7 @@ var CommissionPayoutService = class CommissionPayoutService {
|
|
|
3817
3883
|
const ids = payoutIds.filter((id) => CommissionPayoutService.UUID_RE.test(id));
|
|
3818
3884
|
if (ids.length === 0) return counts;
|
|
3819
3885
|
const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
|
|
3820
|
-
const res = await db.query(`SELECT payout_id, COUNT(*) AS row_count FROM ${table} WHERE payout_id IN (${placeholders}) GROUP BY payout_id`, ...ids);
|
|
3886
|
+
const res = await db.query(`SELECT CAST(payout_id AS VARCHAR) AS payout_id, COUNT(*) AS row_count FROM ${table} WHERE payout_id IN (${placeholders}) GROUP BY payout_id`, ...ids);
|
|
3821
3887
|
const rows = Array.isArray(res) ? res : res.rows ?? [];
|
|
3822
3888
|
for (const row of rows) counts.set(String(row.payout_id), Number(row.row_count));
|
|
3823
3889
|
return counts;
|
|
@@ -4336,6 +4402,6 @@ var EarnerAttributionService = class EarnerAttributionService {
|
|
|
4336
4402
|
}
|
|
4337
4403
|
};
|
|
4338
4404
|
//#endregion
|
|
4339
|
-
export {
|
|
4405
|
+
export { ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as A, PAYOUT_METHODS as B, calculateWaterfallWithPools as C, CommissionPayout as D, CommissionPayoutCollection as E, COMMISSION_STATUSES as F, Commission as G, WATERFALL_POOL_RESIDUAL_RULES as H, EARNER_KINDS as I, CommissionAdjustmentCollection as J, CommissionAdjustmentOperationCollection as K, EARNER_SOURCE_ATTRIBUTION_STATUSES as L, COMMISSION_BASES as M, COMMISSION_PAYOUT_STATUSES as N, Earner as O, COMMISSION_PLAN_STATUSES as P, EARNER_STATUSES as R, calculateWaterfall as S, validateWaterfallPlan as T, WATERFALL_POOL_ZERO_WEIGHT_RULES as U, PAYOUT_METHODS_REQUIRING_DESTINATION as V, CommissionCollection as W, CommissionAdjustment as Y, EarnerCollection as _, CommissionBalanceService as a, validateCommissionPlanComponents as b, CommissionAdjustmentValidationError as c, centsToAmount as d, roundCents as f, EarnerSourceAttribution as g, EarnerSourceAttributionCollection as h, CommissionCalculationService as i, COMMISSION_ADJUSTMENT_KINDS as j, validatePayoutDestination as k, amountToCents as l, EarningEvent as m, CommissionSettlementService as n, CommissionAdjustmentReplayConflictError as o, EarningEventCollection as p, CommissionAdjustmentOperation as q, CommissionPayoutService as r, CommissionAdjustmentService as s, EarnerAttributionService as t, calculateCommissionAmountCents as u, CommissionPlanCollection as v, prepareWaterfallPools as w, WaterfallRecipientResolverRegistry as x, CommissionPlan as y, EARNING_EVENT_KINDS as z };
|
|
4340
4406
|
|
|
4341
|
-
//# sourceMappingURL=commissions-
|
|
4407
|
+
//# sourceMappingURL=commissions-nMBMa0DT.js.map
|