@happyvertical/smrt-sales 0.39.2
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/AGENTS.md +87 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/chunks/__smrt-register__-kWdT7LOt.js +6 -0
- package/dist/chunks/__smrt-register__-kWdT7LOt.js.map +1 -0
- package/dist/chunks/commissions-BCxqOKVM.js +2181 -0
- package/dist/chunks/commissions-BCxqOKVM.js.map +1 -0
- package/dist/chunks/crm-DwEz7E2r.js +1428 -0
- package/dist/chunks/crm-DwEz7E2r.js.map +1 -0
- package/dist/chunks/referrals-BY47p70T.js +2880 -0
- package/dist/chunks/referrals-BY47p70T.js.map +1 -0
- package/dist/commissions/collections/CommissionAdjustmentCollection.d.ts +26 -0
- package/dist/commissions/collections/CommissionAdjustmentCollection.d.ts.map +1 -0
- package/dist/commissions/collections/CommissionCollection.d.ts +43 -0
- package/dist/commissions/collections/CommissionCollection.d.ts.map +1 -0
- package/dist/commissions/collections/CommissionPayoutCollection.d.ts +19 -0
- package/dist/commissions/collections/CommissionPayoutCollection.d.ts.map +1 -0
- package/dist/commissions/collections/CommissionPlanCollection.d.ts +46 -0
- package/dist/commissions/collections/CommissionPlanCollection.d.ts.map +1 -0
- package/dist/commissions/collections/EarnerCollection.d.ts +14 -0
- package/dist/commissions/collections/EarnerCollection.d.ts.map +1 -0
- package/dist/commissions/collections/EarningEventCollection.d.ts +27 -0
- package/dist/commissions/collections/EarningEventCollection.d.ts.map +1 -0
- package/dist/commissions/collections/index.d.ts +11 -0
- package/dist/commissions/collections/index.d.ts.map +1 -0
- package/dist/commissions/index.d.ts +20 -0
- package/dist/commissions/index.d.ts.map +1 -0
- package/dist/commissions/models/Commission.d.ts +150 -0
- package/dist/commissions/models/Commission.d.ts.map +1 -0
- package/dist/commissions/models/CommissionAdjustment.d.ts +62 -0
- package/dist/commissions/models/CommissionAdjustment.d.ts.map +1 -0
- package/dist/commissions/models/CommissionPayout.d.ts +119 -0
- package/dist/commissions/models/CommissionPayout.d.ts.map +1 -0
- package/dist/commissions/models/CommissionPlan.d.ts +117 -0
- package/dist/commissions/models/CommissionPlan.d.ts.map +1 -0
- package/dist/commissions/models/Earner.d.ts +49 -0
- package/dist/commissions/models/Earner.d.ts.map +1 -0
- package/dist/commissions/models/EarningEvent.d.ts +89 -0
- package/dist/commissions/models/EarningEvent.d.ts.map +1 -0
- package/dist/commissions/models/index.d.ts +11 -0
- package/dist/commissions/models/index.d.ts.map +1 -0
- package/dist/commissions/money.d.ts +42 -0
- package/dist/commissions/money.d.ts.map +1 -0
- package/dist/commissions/services/CommissionBalanceService.d.ts +14 -0
- package/dist/commissions/services/CommissionBalanceService.d.ts.map +1 -0
- package/dist/commissions/services/CommissionCalculationService.d.ts +142 -0
- package/dist/commissions/services/CommissionCalculationService.d.ts.map +1 -0
- package/dist/commissions/services/CommissionPayoutService.d.ts +134 -0
- package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -0
- package/dist/commissions/services/CommissionSettlementService.d.ts +42 -0
- package/dist/commissions/services/CommissionSettlementService.d.ts.map +1 -0
- package/dist/commissions/services/index.d.ts +9 -0
- package/dist/commissions/services/index.d.ts.map +1 -0
- package/dist/commissions/types.d.ts +243 -0
- package/dist/commissions/types.d.ts.map +1 -0
- package/dist/commissions.d.ts +2 -0
- package/dist/commissions.d.ts.map +1 -0
- package/dist/commissions.js +3 -0
- package/dist/crm/collections/LeadCollection.d.ts +79 -0
- package/dist/crm/collections/LeadCollection.d.ts.map +1 -0
- package/dist/crm/collections/OpportunityCollection.d.ts +42 -0
- package/dist/crm/collections/OpportunityCollection.d.ts.map +1 -0
- package/dist/crm/collections/OpportunityConversionCollection.d.ts +26 -0
- package/dist/crm/collections/OpportunityConversionCollection.d.ts.map +1 -0
- package/dist/crm/collections/PipelineDefinitionCollection.d.ts +46 -0
- package/dist/crm/collections/PipelineDefinitionCollection.d.ts.map +1 -0
- package/dist/crm/collections/PipelineStageCollection.d.ts +22 -0
- package/dist/crm/collections/PipelineStageCollection.d.ts.map +1 -0
- package/dist/crm/collections/SalesActivityCollection.d.ts +24 -0
- package/dist/crm/collections/SalesActivityCollection.d.ts.map +1 -0
- package/dist/crm/collections/SalesRepresentativeCollection.d.ts +19 -0
- package/dist/crm/collections/SalesRepresentativeCollection.d.ts.map +1 -0
- package/dist/crm/index.d.ts +27 -0
- package/dist/crm/index.d.ts.map +1 -0
- package/dist/crm/models/Lead.d.ts +135 -0
- package/dist/crm/models/Lead.d.ts.map +1 -0
- package/dist/crm/models/Opportunity.d.ts +111 -0
- package/dist/crm/models/Opportunity.d.ts.map +1 -0
- package/dist/crm/models/OpportunityConversion.d.ts +66 -0
- package/dist/crm/models/OpportunityConversion.d.ts.map +1 -0
- package/dist/crm/models/PipelineDefinition.d.ts +58 -0
- package/dist/crm/models/PipelineDefinition.d.ts.map +1 -0
- package/dist/crm/models/PipelineStage.d.ts +66 -0
- package/dist/crm/models/PipelineStage.d.ts.map +1 -0
- package/dist/crm/models/SalesActivity.d.ts +76 -0
- package/dist/crm/models/SalesActivity.d.ts.map +1 -0
- package/dist/crm/models/SalesRepresentative.d.ts +60 -0
- package/dist/crm/models/SalesRepresentative.d.ts.map +1 -0
- package/dist/crm/types.d.ts +228 -0
- package/dist/crm/types.d.ts.map +1 -0
- package/dist/crm.d.ts +2 -0
- package/dist/crm.d.ts.map +1 -0
- package/dist/crm.js +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/manifest.json +9340 -0
- package/dist/referrals/collections/AttributionExceptionCollection.d.ts +17 -0
- package/dist/referrals/collections/AttributionExceptionCollection.d.ts.map +1 -0
- package/dist/referrals/collections/AttributionPolicyCollection.d.ts +50 -0
- package/dist/referrals/collections/AttributionPolicyCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferralAgreementCollection.d.ts +48 -0
- package/dist/referrals/collections/ReferralAgreementCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferralCollection.d.ts +25 -0
- package/dist/referrals/collections/ReferralCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferralLinkCollection.d.ts +96 -0
- package/dist/referrals/collections/ReferralLinkCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferralProgramCollection.d.ts +18 -0
- package/dist/referrals/collections/ReferralProgramCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferralTermSnapshotCollection.d.ts +13 -0
- package/dist/referrals/collections/ReferralTermSnapshotCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferralTouchCollection.d.ts +30 -0
- package/dist/referrals/collections/ReferralTouchCollection.d.ts.map +1 -0
- package/dist/referrals/collections/ReferrerCollection.d.ts +17 -0
- package/dist/referrals/collections/ReferrerCollection.d.ts.map +1 -0
- package/dist/referrals/index.d.ts +38 -0
- package/dist/referrals/index.d.ts.map +1 -0
- package/dist/referrals/models/AttributionException.d.ts +85 -0
- package/dist/referrals/models/AttributionException.d.ts.map +1 -0
- package/dist/referrals/models/AttributionPolicy.d.ts +128 -0
- package/dist/referrals/models/AttributionPolicy.d.ts.map +1 -0
- package/dist/referrals/models/Referral.d.ts +112 -0
- package/dist/referrals/models/Referral.d.ts.map +1 -0
- package/dist/referrals/models/ReferralAgreement.d.ts +125 -0
- package/dist/referrals/models/ReferralAgreement.d.ts.map +1 -0
- package/dist/referrals/models/ReferralLink.d.ts +71 -0
- package/dist/referrals/models/ReferralLink.d.ts.map +1 -0
- package/dist/referrals/models/ReferralProgram.d.ts +57 -0
- package/dist/referrals/models/ReferralProgram.d.ts.map +1 -0
- package/dist/referrals/models/ReferralTermSnapshot.d.ts +61 -0
- package/dist/referrals/models/ReferralTermSnapshot.d.ts.map +1 -0
- package/dist/referrals/models/ReferralTouch.d.ts +55 -0
- package/dist/referrals/models/ReferralTouch.d.ts.map +1 -0
- package/dist/referrals/models/Referrer.d.ts +69 -0
- package/dist/referrals/models/Referrer.d.ts.map +1 -0
- package/dist/referrals/services/AttributionService.d.ts +248 -0
- package/dist/referrals/services/AttributionService.d.ts.map +1 -0
- package/dist/referrals/services/ReferralCommissionService.d.ts +91 -0
- package/dist/referrals/services/ReferralCommissionService.d.ts.map +1 -0
- package/dist/referrals/services/ReferralQualificationService.d.ts +135 -0
- package/dist/referrals/services/ReferralQualificationService.d.ts.map +1 -0
- package/dist/referrals/types.d.ts +226 -0
- package/dist/referrals/types.d.ts.map +1 -0
- package/dist/referrals.d.ts +2 -0
- package/dist/referrals.d.ts.map +1 -0
- package/dist/referrals.js +3 -0
- package/dist/smrt-knowledge.json +5501 -0
- package/dist/svelte/__tests__/format.test.js +68 -0
- package/dist/svelte/__tests__/types.test.js +349 -0
- package/dist/svelte/components/AttributionConflictQueue.svelte +361 -0
- package/dist/svelte/components/AttributionConflictQueue.svelte.d.ts +15 -0
- package/dist/svelte/components/AttributionConflictQueue.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CommissionBreakdown.svelte +251 -0
- package/dist/svelte/components/CommissionBreakdown.svelte.d.ts +11 -0
- package/dist/svelte/components/CommissionBreakdown.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CommissionExpenseSummary.svelte +186 -0
- package/dist/svelte/components/CommissionExpenseSummary.svelte.d.ts +13 -0
- package/dist/svelte/components/CommissionExpenseSummary.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ExecutedAgreementsList.svelte +136 -0
- package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts +11 -0
- package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/LeadList.svelte +224 -0
- package/dist/svelte/components/LeadList.svelte.d.ts +19 -0
- package/dist/svelte/components/LeadList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/OpportunityBoard.svelte +230 -0
- package/dist/svelte/components/OpportunityBoard.svelte.d.ts +17 -0
- package/dist/svelte/components/OpportunityBoard.svelte.d.ts.map +1 -0
- package/dist/svelte/components/OpportunityDetail.svelte +472 -0
- package/dist/svelte/components/OpportunityDetail.svelte.d.ts +27 -0
- package/dist/svelte/components/OpportunityDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PayoutBatchReview.svelte +285 -0
- package/dist/svelte/components/PayoutBatchReview.svelte.d.ts +21 -0
- package/dist/svelte/components/PayoutBatchReview.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PayoutHistoryList.svelte +245 -0
- package/dist/svelte/components/PayoutHistoryList.svelte.d.ts +11 -0
- package/dist/svelte/components/PayoutHistoryList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ReferralLinkManager.svelte +216 -0
- package/dist/svelte/components/ReferralLinkManager.svelte.d.ts +17 -0
- package/dist/svelte/components/ReferralLinkManager.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ReferralStatusList.svelte +118 -0
- package/dist/svelte/components/ReferralStatusList.svelte.d.ts +11 -0
- package/dist/svelte/components/ReferralStatusList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ReferrerEarningsSummary.svelte +106 -0
- package/dist/svelte/components/ReferrerEarningsSummary.svelte.d.ts +11 -0
- package/dist/svelte/components/ReferrerEarningsSummary.svelte.d.ts.map +1 -0
- package/dist/svelte/components/SalesDashboard.svelte +192 -0
- package/dist/svelte/components/SalesDashboard.svelte.d.ts +29 -0
- package/dist/svelte/components/SalesDashboard.svelte.d.ts.map +1 -0
- package/dist/svelte/format.d.ts +46 -0
- package/dist/svelte/format.d.ts.map +1 -0
- package/dist/svelte/format.js +75 -0
- package/dist/svelte/index.d.ts +50 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +37 -0
- package/dist/svelte/types.d.ts +422 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +376 -0
- package/package.json +89 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentational view models and pure helpers for the sales Svelte surfaces.
|
|
3
|
+
*
|
|
4
|
+
* The components are props-driven and presentational: hosts map their model
|
|
5
|
+
* rows (CRM, referrals, commissions) onto these plain view-model interfaces
|
|
6
|
+
* and wire the action callbacks. Only STATUS UNIONS are imported from the
|
|
7
|
+
* sibling TS modules — type-only, so the compiled Svelte bundle never gains a
|
|
8
|
+
* runtime dependency on the model/collection code. Monetary fields stay
|
|
9
|
+
* integer cents in every view model; conversion to display strings happens at
|
|
10
|
+
* render time via `format.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Dashboard math, award validation, and status flows live here as exported
|
|
13
|
+
* pure functions so they are unit-testable without mounting components.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { formatCents, formatPercent } from './format.js';
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// Status → badge-variant mappers (pure)
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Badge variant for a Lead lifecycle status. */
|
|
22
|
+
export function leadStatusBadgeVariant(status) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'new':
|
|
25
|
+
return 'info';
|
|
26
|
+
case 'working':
|
|
27
|
+
return 'primary';
|
|
28
|
+
case 'qualified':
|
|
29
|
+
return 'success';
|
|
30
|
+
case 'disqualified':
|
|
31
|
+
return 'error';
|
|
32
|
+
default:
|
|
33
|
+
return 'default';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Badge variant for an Opportunity lifecycle status. */
|
|
37
|
+
export function opportunityStatusBadgeVariant(status) {
|
|
38
|
+
switch (status) {
|
|
39
|
+
case 'won':
|
|
40
|
+
return 'success';
|
|
41
|
+
case 'lost':
|
|
42
|
+
return 'error';
|
|
43
|
+
default:
|
|
44
|
+
return 'info';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Badge variant for a Referral lifecycle status. */
|
|
48
|
+
export function referralStatusBadgeVariant(status) {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 'attributed':
|
|
51
|
+
return 'info';
|
|
52
|
+
case 'qualified':
|
|
53
|
+
return 'success';
|
|
54
|
+
case 'disqualified':
|
|
55
|
+
return 'error';
|
|
56
|
+
case 'expired':
|
|
57
|
+
return 'warning';
|
|
58
|
+
case 'under_review':
|
|
59
|
+
return 'warning';
|
|
60
|
+
default:
|
|
61
|
+
return 'default';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Badge variant for a ReferralLink status. */
|
|
65
|
+
export function referralLinkStatusBadgeVariant(status) {
|
|
66
|
+
return status === 'active' ? 'success' : 'default';
|
|
67
|
+
}
|
|
68
|
+
/** Badge variant for a Commission settlement-chain status. */
|
|
69
|
+
export function commissionStatusBadgeVariant(status) {
|
|
70
|
+
switch (status) {
|
|
71
|
+
case 'earned':
|
|
72
|
+
return 'info';
|
|
73
|
+
case 'approved':
|
|
74
|
+
return 'primary';
|
|
75
|
+
case 'payable':
|
|
76
|
+
return 'warning';
|
|
77
|
+
case 'paid':
|
|
78
|
+
return 'success';
|
|
79
|
+
default:
|
|
80
|
+
return 'default';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Badge variant for a CommissionPayout batch status. */
|
|
84
|
+
export function payoutStatusBadgeVariant(status) {
|
|
85
|
+
switch (status) {
|
|
86
|
+
case 'approved':
|
|
87
|
+
return 'info';
|
|
88
|
+
case 'processing':
|
|
89
|
+
return 'warning';
|
|
90
|
+
case 'completed':
|
|
91
|
+
return 'success';
|
|
92
|
+
case 'failed':
|
|
93
|
+
return 'error';
|
|
94
|
+
default:
|
|
95
|
+
return 'default';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Badge variant for a ReferralAgreement version status. */
|
|
99
|
+
export function agreementStatusBadgeVariant(status) {
|
|
100
|
+
switch (status) {
|
|
101
|
+
case 'active':
|
|
102
|
+
return 'success';
|
|
103
|
+
case 'superseded':
|
|
104
|
+
return 'warning';
|
|
105
|
+
case 'terminated':
|
|
106
|
+
return 'error';
|
|
107
|
+
default:
|
|
108
|
+
return 'default';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// CRM helpers (pure)
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Whether the qualify action applies to a lead in `status` — mirrors the
|
|
116
|
+
* `new|working → qualified` transitions guarded by the Lead model.
|
|
117
|
+
*/
|
|
118
|
+
export function canQualifyLead(status) {
|
|
119
|
+
return status === 'new' || status === 'working';
|
|
120
|
+
}
|
|
121
|
+
/** Whether a next action is overdue relative to `now` (default: current time). */
|
|
122
|
+
export function isOverdue(dueAt, now = new Date()) {
|
|
123
|
+
if (dueAt == null || dueAt === '')
|
|
124
|
+
return false;
|
|
125
|
+
const due = dueAt instanceof Date ? dueAt : new Date(dueAt);
|
|
126
|
+
if (Number.isNaN(due.getTime()))
|
|
127
|
+
return false;
|
|
128
|
+
return due.getTime() < now.getTime();
|
|
129
|
+
}
|
|
130
|
+
/** Count of opportunities still `open`. */
|
|
131
|
+
export function openOpportunityCount(opportunities) {
|
|
132
|
+
return opportunities.filter((o) => o.status === 'open').length;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Total expected value of OPEN opportunities, grouped per currency (sorted by
|
|
136
|
+
* currency code) — currencies are never summed together.
|
|
137
|
+
*/
|
|
138
|
+
export function openPipelineTotals(opportunities) {
|
|
139
|
+
const totals = new Map();
|
|
140
|
+
for (const opportunity of opportunities) {
|
|
141
|
+
if (opportunity.status !== 'open')
|
|
142
|
+
continue;
|
|
143
|
+
totals.set(opportunity.currency, (totals.get(opportunity.currency) ?? 0) + opportunity.expectedValueCents);
|
|
144
|
+
}
|
|
145
|
+
return [...totals.entries()]
|
|
146
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
147
|
+
.map(([currency, amountCents]) => ({ currency, amountCents }));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Open-pipeline expected value per stage, in the given stage order. Terminal
|
|
151
|
+
* stages report zero (their opportunities are no longer `open`).
|
|
152
|
+
*/
|
|
153
|
+
export function pipelineValueByStage(stages, opportunities) {
|
|
154
|
+
return stages.map((stage) => {
|
|
155
|
+
const open = opportunities.filter((o) => o.stageId === stage.id && o.status === 'open');
|
|
156
|
+
return {
|
|
157
|
+
stageId: stage.id,
|
|
158
|
+
stageName: stage.name,
|
|
159
|
+
openCount: open.length,
|
|
160
|
+
totals: openPipelineTotals(open),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Win rate over CLOSED opportunities: `won / (won + lost)`, or `null` when
|
|
166
|
+
* nothing has closed yet (so the tile can render a placeholder, not `0%`).
|
|
167
|
+
*/
|
|
168
|
+
export function winRate(opportunities) {
|
|
169
|
+
let won = 0;
|
|
170
|
+
let lost = 0;
|
|
171
|
+
for (const opportunity of opportunities) {
|
|
172
|
+
if (opportunity.status === 'won')
|
|
173
|
+
won += 1;
|
|
174
|
+
else if (opportunity.status === 'lost')
|
|
175
|
+
lost += 1;
|
|
176
|
+
}
|
|
177
|
+
const closed = won + lost;
|
|
178
|
+
return closed === 0 ? null : won / closed;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Group opportunities into the given stage order for the board. Opportunities
|
|
182
|
+
* pointing at a stage not present in `stages` are omitted — the board renders
|
|
183
|
+
* exactly the columns it is given.
|
|
184
|
+
*/
|
|
185
|
+
export function groupOpportunitiesByStage(stages, opportunities) {
|
|
186
|
+
return stages.map((stage) => ({
|
|
187
|
+
stage,
|
|
188
|
+
opportunities: opportunities.filter((o) => o.stageId === stage.id),
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/** Neighbouring stage ids for keyboard-accessible next/prev stage movement. */
|
|
192
|
+
export function adjacentStageIds(stages, stageId) {
|
|
193
|
+
const index = stages.findIndex((stage) => stage.id === stageId);
|
|
194
|
+
if (index === -1)
|
|
195
|
+
return { prevStageId: null, nextStageId: null };
|
|
196
|
+
return {
|
|
197
|
+
prevStageId: index > 0 ? stages[index - 1].id : null,
|
|
198
|
+
nextStageId: index < stages.length - 1 ? stages[index + 1].id : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// Referral helpers (pure)
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Share URL for a referral code: `<shareBaseUrl>/<code>` (trailing slashes on
|
|
206
|
+
* the base are normalised; the code is URL-encoded).
|
|
207
|
+
*/
|
|
208
|
+
export function buildShareUrl(shareBaseUrl, code) {
|
|
209
|
+
const base = shareBaseUrl.replace(/\/+$/, '');
|
|
210
|
+
return `${base}/${encodeURIComponent(code)}`;
|
|
211
|
+
}
|
|
212
|
+
/** Loose http(s) URL check used to gate the create-link form. */
|
|
213
|
+
export function isHttpUrl(value) {
|
|
214
|
+
return /^https?:\/\/\S+$/i.test(value.trim());
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Tolerance for award credit fractions summing to 1.0 — mirrors
|
|
218
|
+
* `AttributionService.assertAwards` (±0.0001).
|
|
219
|
+
*/
|
|
220
|
+
export const AWARD_FRACTION_TOLERANCE = 0.0001;
|
|
221
|
+
/**
|
|
222
|
+
* Validate an award draft the way `AttributionService.resolveException` will:
|
|
223
|
+
* at least one award, distinct referrers, every fraction in (0, 1], and the
|
|
224
|
+
* fractions summing to 1.0 (±{@link AWARD_FRACTION_TOLERANCE}).
|
|
225
|
+
*/
|
|
226
|
+
export function validateAwards(awards) {
|
|
227
|
+
if (awards.length === 0) {
|
|
228
|
+
return {
|
|
229
|
+
valid: false,
|
|
230
|
+
totalFraction: 0,
|
|
231
|
+
message: 'At least one award is required.',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
let sum = 0;
|
|
236
|
+
for (const award of awards) {
|
|
237
|
+
if (!award.referrerId) {
|
|
238
|
+
return {
|
|
239
|
+
valid: false,
|
|
240
|
+
totalFraction: sum,
|
|
241
|
+
message: 'Every award needs a referrer.',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (seen.has(award.referrerId)) {
|
|
245
|
+
return {
|
|
246
|
+
valid: false,
|
|
247
|
+
totalFraction: sum,
|
|
248
|
+
message: 'Each referrer may receive at most one award.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
seen.add(award.referrerId);
|
|
252
|
+
if (!Number.isFinite(award.creditFraction) ||
|
|
253
|
+
award.creditFraction <= 0 ||
|
|
254
|
+
award.creditFraction > 1) {
|
|
255
|
+
return {
|
|
256
|
+
valid: false,
|
|
257
|
+
totalFraction: sum,
|
|
258
|
+
message: 'Each credit fraction must be greater than 0 and at most 1.',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
sum += award.creditFraction;
|
|
262
|
+
}
|
|
263
|
+
if (Math.abs(sum - 1) > AWARD_FRACTION_TOLERANCE) {
|
|
264
|
+
return {
|
|
265
|
+
valid: false,
|
|
266
|
+
totalFraction: sum,
|
|
267
|
+
message: `Credit fractions must sum to 100% — currently ${formatPercent(sum)}.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { valid: true, totalFraction: sum };
|
|
271
|
+
}
|
|
272
|
+
/** Distinct candidate referrer ids, in first-seen order. */
|
|
273
|
+
export function uniqueCandidateReferrerIds(candidates) {
|
|
274
|
+
const ids = [];
|
|
275
|
+
const seen = new Set();
|
|
276
|
+
for (const candidate of candidates) {
|
|
277
|
+
if (seen.has(candidate.referrerId))
|
|
278
|
+
continue;
|
|
279
|
+
seen.add(candidate.referrerId);
|
|
280
|
+
ids.push(candidate.referrerId);
|
|
281
|
+
}
|
|
282
|
+
return ids;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Seed an equal split across `referrerIds`, rounded to 4 decimal places with
|
|
286
|
+
* the LAST share adjusted so the set sums to exactly 1.0 (mirrors the
|
|
287
|
+
* AttributionService split convention).
|
|
288
|
+
*/
|
|
289
|
+
export function equalSplitAwards(referrerIds) {
|
|
290
|
+
const count = referrerIds.length;
|
|
291
|
+
if (count === 0)
|
|
292
|
+
return [];
|
|
293
|
+
const share = Math.round(10000 / count) / 10000;
|
|
294
|
+
return referrerIds.map((referrerId, index) => ({
|
|
295
|
+
referrerId,
|
|
296
|
+
creditFraction: index === count - 1
|
|
297
|
+
? Math.round((1 - share * (count - 1)) * 10000) / 10000
|
|
298
|
+
: share,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
302
|
+
// Commission / settlement helpers (pure)
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
304
|
+
/** `planKey@vN` display reference for snapshotted plan versions. */
|
|
305
|
+
export function formatPlanRef(planKey, planVersion) {
|
|
306
|
+
return `${planKey}@v${planVersion}`;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Render the snapshotted calculation as a one-line formula. Fixed-basis
|
|
310
|
+
* components have no rate factor (`rate` is recorded as `0`), so the line
|
|
311
|
+
* becomes `base (fixed) × share = amount`.
|
|
312
|
+
*/
|
|
313
|
+
export function formatCommissionFormula(parts, locale) {
|
|
314
|
+
const base = formatCents(parts.baseAmountCents, parts.currency, locale);
|
|
315
|
+
const share = formatPercent(parts.shareFraction, locale);
|
|
316
|
+
const amount = formatCents(parts.amountCents, parts.currency, locale);
|
|
317
|
+
if (parts.basis === 'fixed') {
|
|
318
|
+
return `${base} (fixed) × ${share} = ${amount}`;
|
|
319
|
+
}
|
|
320
|
+
return `${base} × ${formatPercent(parts.rate, locale)} × ${share} = ${amount}`;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Linear settlement timeline for a payout batch. The happy path is
|
|
324
|
+
* `pending → approved → processing → completed`; for a `failed` batch the
|
|
325
|
+
* terminal marker replaces `completed`.
|
|
326
|
+
*/
|
|
327
|
+
export function payoutStatusTimeline(status) {
|
|
328
|
+
const path = status === 'failed'
|
|
329
|
+
? ['pending', 'approved', 'processing', 'failed']
|
|
330
|
+
: ['pending', 'approved', 'processing', 'completed'];
|
|
331
|
+
const index = path.indexOf(status);
|
|
332
|
+
return path.map((step, i) => ({
|
|
333
|
+
status: step,
|
|
334
|
+
state: i < index ? 'done' : i === index ? 'current' : 'upcoming',
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Action gating for {@link PayoutBatchReview}, mirroring the CommissionPayout
|
|
339
|
+
* transition guard: `pending → approved → processing → completed | failed`,
|
|
340
|
+
* with `failed` reachable from `approved`/`processing`.
|
|
341
|
+
*/
|
|
342
|
+
export function payoutActionsFor(status) {
|
|
343
|
+
return {
|
|
344
|
+
canApprove: status === 'pending',
|
|
345
|
+
canMarkProcessing: status === 'approved',
|
|
346
|
+
canComplete: status === 'processing',
|
|
347
|
+
canFail: status === 'approved' || status === 'processing',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Sum reconciliation rows per currency (sorted by currency code). Currencies
|
|
352
|
+
* are never summed together.
|
|
353
|
+
*/
|
|
354
|
+
export function sumExpenseRowsByCurrency(rows) {
|
|
355
|
+
const byCurrency = new Map();
|
|
356
|
+
for (const row of rows) {
|
|
357
|
+
const entry = byCurrency.get(row.currency) ?? {
|
|
358
|
+
expense: 0,
|
|
359
|
+
adjustments: 0,
|
|
360
|
+
payouts: 0,
|
|
361
|
+
};
|
|
362
|
+
entry.expense += row.commissionExpenseCents;
|
|
363
|
+
entry.adjustments += row.adjustmentCents;
|
|
364
|
+
entry.payouts += row.payoutCents;
|
|
365
|
+
byCurrency.set(row.currency, entry);
|
|
366
|
+
}
|
|
367
|
+
return [...byCurrency.entries()]
|
|
368
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
369
|
+
.map(([currency, { expense, adjustments, payouts }]) => ({
|
|
370
|
+
currency,
|
|
371
|
+
commissionExpenseCents: expense,
|
|
372
|
+
adjustmentCents: adjustments,
|
|
373
|
+
payoutCents: payouts,
|
|
374
|
+
netAccruedCents: expense + adjustments - payouts,
|
|
375
|
+
}));
|
|
376
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@happyvertical/smrt-sales",
|
|
3
|
+
"version": "0.39.2",
|
|
4
|
+
"description": "Modular sales for the SMRT framework: CRM (Leads, Opportunities, Pipelines), referral intake with versioned attribution policies, neutral commissions with snapshots and payouts, and reusable Svelte surfaces",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"CLAUDE.md",
|
|
11
|
+
"AGENTS.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./crm": {
|
|
19
|
+
"types": "./dist/crm.d.ts",
|
|
20
|
+
"import": "./dist/crm.js"
|
|
21
|
+
},
|
|
22
|
+
"./referrals": {
|
|
23
|
+
"types": "./dist/referrals.d.ts",
|
|
24
|
+
"import": "./dist/referrals.js"
|
|
25
|
+
},
|
|
26
|
+
"./commissions": {
|
|
27
|
+
"types": "./dist/commissions.d.ts",
|
|
28
|
+
"import": "./dist/commissions.js"
|
|
29
|
+
},
|
|
30
|
+
"./svelte": {
|
|
31
|
+
"types": "./dist/svelte/index.d.ts",
|
|
32
|
+
"svelte": "./dist/svelte/index.js",
|
|
33
|
+
"import": "./dist/svelte/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./manifest": "./dist/manifest.json",
|
|
36
|
+
"./manifest.json": "./dist/manifest.json"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@happyvertical/sql": "^0.78.0",
|
|
40
|
+
"@happyvertical/smrt-core": "0.39.2",
|
|
41
|
+
"@happyvertical/smrt-tenancy": "0.39.2",
|
|
42
|
+
"@happyvertical/smrt-ui": "0.39.2"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"svelte": "^5.56.4"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@sveltejs/package": "^2.5.8",
|
|
49
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
50
|
+
"@types/node": "24.13.2",
|
|
51
|
+
"svelte": "^5.56.4",
|
|
52
|
+
"svelte-check": "^4.7.1",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vite": "^8.1.3",
|
|
55
|
+
"vitest": "^4.1.9",
|
|
56
|
+
"@happyvertical/smrt-vitest": "0.39.2"
|
|
57
|
+
},
|
|
58
|
+
"smrtRawPrimitives": "strict",
|
|
59
|
+
"keywords": [
|
|
60
|
+
"smrt",
|
|
61
|
+
"sales",
|
|
62
|
+
"crm",
|
|
63
|
+
"referrals",
|
|
64
|
+
"commissions",
|
|
65
|
+
"payouts",
|
|
66
|
+
"multi-tenant"
|
|
67
|
+
],
|
|
68
|
+
"author": "HappyVertical",
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"registry": "https://registry.npmjs.org",
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/happyvertical/smrt.git",
|
|
77
|
+
"directory": "packages/sales"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json",
|
|
81
|
+
"build:watch": "vite build --mode library --watch",
|
|
82
|
+
"clean": "rm -rf dist",
|
|
83
|
+
"dev": "vite dev",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:watch": "vitest",
|
|
86
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",
|
|
87
|
+
"verify:pack": "node ../../scripts/verify-package-types-exports.js ."
|
|
88
|
+
}
|
|
89
|
+
}
|