-
Notifications
You must be signed in to change notification settings - Fork 245
Expand file tree
/
Copy pathmatchCustomerLicenseSuccessors.ts
More file actions
106 lines (95 loc) · 3.51 KB
/
Copy pathmatchCustomerLicenseSuccessors.ts
File metadata and controls
106 lines (95 loc) · 3.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import type { FullCustomerLicense } from "@autumn/shared";
export type CustomerLicenseSuccessorMatch = {
outgoingCustomerLicense: FullCustomerLicense;
incomingCustomerLicense: FullCustomerLicense;
};
export type UnmatchedOutgoingCustomerLicense = {
outgoingCustomerLicense: FullCustomerLicense;
reason: "dropped" | "ambiguous";
group?: string;
};
const licensePlanIdOf = (customerLicense: FullCustomerLicense) =>
customerLicense.planLicense?.product.id ??
customerLicense.license_internal_product_id;
/** Empty group never cross-pairs — grouping is the explicit opt-in for
* "this is the same seat across plans". */
const licenseGroupOf = (customerLicense: FullCustomerLicense) =>
customerLicense.planLicense?.product.group || null;
/**
* Ranked successor selection for license pools across a parent plan
* transition: the same license plan id always wins; otherwise pools pair by
* their license plan's group, but only when the group resolves 1:1 among the
* pools no id claimed. Several candidates on either side match nothing.
*/
export const matchCustomerLicenseSuccessors = ({
outgoingCustomerLicenses,
incomingCustomerLicenses,
}: {
outgoingCustomerLicenses: FullCustomerLicense[];
incomingCustomerLicenses: FullCustomerLicense[];
}): {
matches: CustomerLicenseSuccessorMatch[];
unmatched: UnmatchedOutgoingCustomerLicense[];
} => {
const incomingByLicensePlanId = new Map<string, FullCustomerLicense>();
for (const incoming of incomingCustomerLicenses) {
const licensePlanId = licensePlanIdOf(incoming);
if (!incomingByLicensePlanId.has(licensePlanId)) {
incomingByLicensePlanId.set(licensePlanId, incoming);
}
}
const idClaimedIncoming = new Set<FullCustomerLicense>();
for (const outgoing of outgoingCustomerLicenses) {
const claimed = incomingByLicensePlanId.get(licensePlanIdOf(outgoing));
if (claimed) idClaimedIncoming.add(claimed);
}
const incomingByGroup = new Map<string, FullCustomerLicense[]>();
for (const incoming of incomingCustomerLicenses) {
if (idClaimedIncoming.has(incoming)) continue;
const group = licenseGroupOf(incoming);
if (!group) continue;
const rows = incomingByGroup.get(group);
if (rows) rows.push(incoming);
else incomingByGroup.set(group, [incoming]);
}
const outgoingGroupCounts = new Map<string, number>();
for (const outgoing of outgoingCustomerLicenses) {
if (incomingByLicensePlanId.has(licensePlanIdOf(outgoing))) continue;
const group = licenseGroupOf(outgoing);
if (!group) continue;
outgoingGroupCounts.set(group, (outgoingGroupCounts.get(group) ?? 0) + 1);
}
const matches: CustomerLicenseSuccessorMatch[] = [];
const unmatched: UnmatchedOutgoingCustomerLicense[] = [];
for (const outgoing of outgoingCustomerLicenses) {
const idMatch = incomingByLicensePlanId.get(licensePlanIdOf(outgoing));
if (idMatch) {
matches.push({
outgoingCustomerLicense: outgoing,
incomingCustomerLicense: idMatch,
});
continue;
}
const group = licenseGroupOf(outgoing);
const candidates = group ? (incomingByGroup.get(group) ?? []) : [];
if (candidates.length === 0) {
unmatched.push({ outgoingCustomerLicense: outgoing, reason: "dropped" });
continue;
}
const ambiguous =
candidates.length > 1 || (outgoingGroupCounts.get(group ?? "") ?? 0) > 1;
if (ambiguous) {
unmatched.push({
outgoingCustomerLicense: outgoing,
reason: "ambiguous",
group: group ?? undefined,
});
continue;
}
matches.push({
outgoingCustomerLicense: outgoing,
incomingCustomerLicense: candidates[0],
});
}
return { matches, unmatched };
};