@go-avro/avro-js 0.0.49 → 0.0.50
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/dist/billing/fees.d.ts +40 -0
- package/dist/billing/fees.js +155 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types/api/Bill.d.ts +4 -1
- package/dist/types/api/Company.d.ts +1 -0
- package/dist/types/api/LineItem.d.ts +1 -0
- package/dist/types/api/LineItem.js +1 -0
- package/dist/types/api/ProcessingFeeLineItem.d.ts +21 -0
- package/dist/types/api/ProcessingFeeLineItem.js +1 -0
- package/dist/types/api.d.ts +1 -0
- package/dist/types/api.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PaymentType } from '../types/api/PaymentType';
|
|
2
|
+
export interface PassOnFeeBreakdown {
|
|
3
|
+
/** Stripe processing fee on the grossed-up amount, in cents. */
|
|
4
|
+
stripeFee: number;
|
|
5
|
+
/** Avro application fee on the net (gross - stripeFee), in cents. */
|
|
6
|
+
avroFee: number;
|
|
7
|
+
/** Total fee added on top of the subtotal: stripeFee + avroFee. */
|
|
8
|
+
totalFee: number;
|
|
9
|
+
/** Gross amount the customer is charged: subtotal + totalFee. */
|
|
10
|
+
gross: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compute Stripe's processing fee on a gross amount, in cents.
|
|
14
|
+
* Ceiling matches Stripe's worst-case rounding so the merchant is never short.
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeStripeFee(grossCents: number, method: PaymentType): number;
|
|
17
|
+
/**
|
|
18
|
+
* Compute Avro's application fee on a net amount (gross - stripeFee), in cents.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeAvroFee(netCents: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Compute the pass-on fee (in cents) such that, after Stripe and Avro deduct
|
|
23
|
+
* their fees from the gross amount the customer is charged, the merchant
|
|
24
|
+
* receives at least `subtotalCents`.
|
|
25
|
+
*
|
|
26
|
+
* Returns `ZERO_FEE` when `subtotalCents <= 0`.
|
|
27
|
+
*
|
|
28
|
+
* Closed form: a finite set of candidate grosses is derived from each fee
|
|
29
|
+
* regime (avro at the 5¢ floor vs. percentage; ACH stripe at the $5 cap vs.
|
|
30
|
+
* percentage). The smallest candidate that satisfies the invariant
|
|
31
|
+
* `gross - stripeFee(gross) - avroFee(gross - stripeFee(gross)) >= subtotal`
|
|
32
|
+
* is selected. No iteration.
|
|
33
|
+
*/
|
|
34
|
+
export declare function computePassOnFee(subtotalCents: number, method: PaymentType): PassOnFeeBreakdown;
|
|
35
|
+
/**
|
|
36
|
+
* Pick the payment method to use when computing the persisted ("default")
|
|
37
|
+
* fee line item amount on a bill. Card if enabled (worst case for the
|
|
38
|
+
* customer), otherwise ACH, otherwise null (no fee should be persisted).
|
|
39
|
+
*/
|
|
40
|
+
export declare function defaultPassOnFeeMethod(enabledPaymentMethods: PaymentType[] | null | undefined): PaymentType | null;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { PaymentType } from '../types/api/PaymentType';
|
|
2
|
+
/**
|
|
3
|
+
* Pass-on (surcharge) fee calculations.
|
|
4
|
+
*
|
|
5
|
+
* When `pass_on_fees` is enabled on a bill, the customer is charged a gross
|
|
6
|
+
* amount that, after Stripe's processing fees and Avro's application fee, leaves
|
|
7
|
+
* the merchant with the bill's subtotal. The fee added on top of the subtotal
|
|
8
|
+
* is exposed as a single read-only line item on the bill.
|
|
9
|
+
*
|
|
10
|
+
* Stripe US standard pricing (online):
|
|
11
|
+
* - Card: 2.9% of gross + 30¢
|
|
12
|
+
* - ACH (us_bank): 0.8% of gross, capped at $5.00
|
|
13
|
+
* Avro application fee:
|
|
14
|
+
* - 0.4% of net (gross - stripe), minimum 5¢
|
|
15
|
+
*
|
|
16
|
+
* All amounts are integer cents. All math is closed-form (no iteration).
|
|
17
|
+
*
|
|
18
|
+
* The integer-ceiling on the gross-up guarantees the merchant nets at
|
|
19
|
+
* least the requested subtotal. In rare regime-boundary cases the merchant
|
|
20
|
+
* may net a sub-cent more than requested — never less.
|
|
21
|
+
*/
|
|
22
|
+
// --- Rate constants (per 1000 for integer math where applicable) ---
|
|
23
|
+
const CARD_PCT_PER_1000 = 29; // 2.9%
|
|
24
|
+
const CARD_FIXED_CENTS = 30; // 30¢
|
|
25
|
+
const ACH_PCT_PER_1000 = 8; // 0.8%
|
|
26
|
+
const ACH_CAP_CENTS = 500; // $5.00
|
|
27
|
+
const AVRO_PCT_PER_1000 = 4; // 0.4%
|
|
28
|
+
const AVRO_MIN_CENTS = 5; // 5¢
|
|
29
|
+
const ZERO_FEE = {
|
|
30
|
+
stripeFee: 0,
|
|
31
|
+
avroFee: 0,
|
|
32
|
+
totalFee: 0,
|
|
33
|
+
gross: 0,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Compute Stripe's processing fee on a gross amount, in cents.
|
|
37
|
+
* Ceiling matches Stripe's worst-case rounding so the merchant is never short.
|
|
38
|
+
*/
|
|
39
|
+
export function computeStripeFee(grossCents, method) {
|
|
40
|
+
if (grossCents <= 0)
|
|
41
|
+
return 0;
|
|
42
|
+
if (method === PaymentType.CARD) {
|
|
43
|
+
return Math.ceil((CARD_PCT_PER_1000 * grossCents) / 1000) + CARD_FIXED_CENTS;
|
|
44
|
+
}
|
|
45
|
+
// us_bank_account: percentage capped at $5.00
|
|
46
|
+
return Math.min(Math.ceil((ACH_PCT_PER_1000 * grossCents) / 1000), ACH_CAP_CENTS);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Compute Avro's application fee on a net amount (gross - stripeFee), in cents.
|
|
50
|
+
*/
|
|
51
|
+
export function computeAvroFee(netCents) {
|
|
52
|
+
if (netCents <= 0)
|
|
53
|
+
return AVRO_MIN_CENTS;
|
|
54
|
+
return Math.max(Math.ceil((AVRO_PCT_PER_1000 * netCents) / 1000), AVRO_MIN_CENTS);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute the pass-on fee (in cents) such that, after Stripe and Avro deduct
|
|
58
|
+
* their fees from the gross amount the customer is charged, the merchant
|
|
59
|
+
* receives at least `subtotalCents`.
|
|
60
|
+
*
|
|
61
|
+
* Returns `ZERO_FEE` when `subtotalCents <= 0`.
|
|
62
|
+
*
|
|
63
|
+
* Closed form: a finite set of candidate grosses is derived from each fee
|
|
64
|
+
* regime (avro at the 5¢ floor vs. percentage; ACH stripe at the $5 cap vs.
|
|
65
|
+
* percentage). The smallest candidate that satisfies the invariant
|
|
66
|
+
* `gross - stripeFee(gross) - avroFee(gross - stripeFee(gross)) >= subtotal`
|
|
67
|
+
* is selected. No iteration.
|
|
68
|
+
*/
|
|
69
|
+
export function computePassOnFee(subtotalCents, method) {
|
|
70
|
+
if (subtotalCents <= 0)
|
|
71
|
+
return { ...ZERO_FEE };
|
|
72
|
+
const B = subtotalCents;
|
|
73
|
+
const candidates = method === PaymentType.CARD ? cardCandidates(B) : achCandidates(B);
|
|
74
|
+
let best = Number.POSITIVE_INFINITY;
|
|
75
|
+
for (const g of candidates) {
|
|
76
|
+
const stripe = computeStripeFee(g, method);
|
|
77
|
+
const avro = computeAvroFee(g - stripe);
|
|
78
|
+
if (g - stripe - avro >= B && g < best) {
|
|
79
|
+
best = g;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isFinite(best)) {
|
|
83
|
+
// Should never happen — the candidate set covers every regime — but
|
|
84
|
+
// fall back to a safe overshoot rather than throwing.
|
|
85
|
+
best = B + Math.ceil(B * 0.05) + CARD_FIXED_CENTS + AVRO_MIN_CENTS;
|
|
86
|
+
}
|
|
87
|
+
const stripeFee = computeStripeFee(best, method);
|
|
88
|
+
const avroFee = computeAvroFee(best - stripeFee);
|
|
89
|
+
return {
|
|
90
|
+
stripeFee,
|
|
91
|
+
avroFee,
|
|
92
|
+
totalFee: best - B,
|
|
93
|
+
gross: best,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Card regime candidates.
|
|
98
|
+
*
|
|
99
|
+
* Stripe = ceil(0.029 * G) + 30; avro = max(ceil(0.004 * (G - stripe)), 5).
|
|
100
|
+
*
|
|
101
|
+
* The two `ceil`s can each add up to 1¢ relative to the exact rational form,
|
|
102
|
+
* so each candidate carries a baked-in 2¢ (percentage regime) or 1¢ (floor
|
|
103
|
+
* regime) safety margin to guarantee the invariant holds for every B in
|
|
104
|
+
* integer cents — no post-hoc iteration required.
|
|
105
|
+
*
|
|
106
|
+
* 1) Percentage avro: G * 0.967116 ≥ B + 29.88 + 2
|
|
107
|
+
* => G = ceil((1_000_000 * B + 31_880_000) / 967_116)
|
|
108
|
+
* 2) Floor avro (5¢): G * 0.971 ≥ B + 35 + 1
|
|
109
|
+
* => G = ceil((1000 * B + 36_000) / 971)
|
|
110
|
+
*/
|
|
111
|
+
function cardCandidates(B) {
|
|
112
|
+
return [
|
|
113
|
+
Math.ceil((1000000 * B + 31880000) / 967116),
|
|
114
|
+
Math.ceil((1000 * B + 36000) / 971),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* ACH regime candidates.
|
|
119
|
+
*
|
|
120
|
+
* Stripe = min(ceil(0.008 * G), 500); avro = max(ceil(0.004 * (G - stripe)), 5).
|
|
121
|
+
*
|
|
122
|
+
* Same ceiling-safety logic as card. Stripe at the $5 cap is a fixed integer
|
|
123
|
+
* so its ceiling-slack disappears in the capped regimes.
|
|
124
|
+
*
|
|
125
|
+
* 1) No-cap percentage avro: G * 0.988032 ≥ B + 2
|
|
126
|
+
* => G = ceil((1_000_000 * (B + 2)) / 988_032)
|
|
127
|
+
* 2) No-cap floor avro: G * 0.992 ≥ B + 5 + 1
|
|
128
|
+
* => G = ceil((1000 * (B + 6)) / 992)
|
|
129
|
+
* 3) Capped percentage avro: G * 0.996 ≥ B + 498 + 1
|
|
130
|
+
* => G = ceil((1000 * (B + 499)) / 996)
|
|
131
|
+
* 4) Capped floor avro: G = B + 505
|
|
132
|
+
*/
|
|
133
|
+
function achCandidates(B) {
|
|
134
|
+
return [
|
|
135
|
+
Math.ceil((1000000 * (B + 2)) / 988032),
|
|
136
|
+
Math.ceil((1000 * (B + 6)) / 992),
|
|
137
|
+
Math.ceil((1000 * (B + 499)) / 996),
|
|
138
|
+
B + 505,
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Pick the payment method to use when computing the persisted ("default")
|
|
143
|
+
* fee line item amount on a bill. Card if enabled (worst case for the
|
|
144
|
+
* customer), otherwise ACH, otherwise null (no fee should be persisted).
|
|
145
|
+
*/
|
|
146
|
+
export function defaultPassOnFeeMethod(enabledPaymentMethods) {
|
|
147
|
+
if (!enabledPaymentMethods || enabledPaymentMethods.length === 0)
|
|
148
|
+
return null;
|
|
149
|
+
if (enabledPaymentMethods.includes(PaymentType.CARD))
|
|
150
|
+
return PaymentType.CARD;
|
|
151
|
+
if (enabledPaymentMethods.includes(PaymentType.US_BANK_ACCOUNT)) {
|
|
152
|
+
return PaymentType.US_BANK_ACCOUNT;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ import './client/hooks/proposal';
|
|
|
32
32
|
import './client/hooks/timecards';
|
|
33
33
|
import './client/hooks/waivers';
|
|
34
34
|
import './client/hooks/email';
|
|
35
|
+
export { computePassOnFee, computeStripeFee, computeAvroFee, defaultPassOnFeeMethod, } from './billing/fees';
|
|
36
|
+
export type { PassOnFeeBreakdown } from './billing/fees';
|
|
35
37
|
export * from './types/api';
|
|
36
38
|
export * from './types/auth';
|
|
37
39
|
export * from './types/cache';
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import './client/hooks/proposal';
|
|
|
30
30
|
import './client/hooks/timecards';
|
|
31
31
|
import './client/hooks/waivers';
|
|
32
32
|
import './client/hooks/email';
|
|
33
|
+
export { computePassOnFee, computeStripeFee, computeAvroFee, defaultPassOnFeeMethod, } from './billing/fees';
|
|
33
34
|
export * from './types/api';
|
|
34
35
|
export * from './types/auth';
|
|
35
36
|
export * from './types/cache';
|
package/dist/types/api/Bill.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { BillPayment } from '../../types/api/BillPayment';
|
|
|
2
2
|
import { BillUser } from '../../types/api/BillUser';
|
|
3
3
|
import { CustomLineItem } from '../../types/api/CustomLineItem';
|
|
4
4
|
import { PaymentType } from '../../types/api/PaymentType';
|
|
5
|
+
import { ProcessingFeeLineItem } from '../../types/api/ProcessingFeeLineItem';
|
|
6
|
+
export type BillLineItem = CustomLineItem | ProcessingFeeLineItem;
|
|
5
7
|
export declare const BillStatus: {
|
|
6
8
|
readonly SENT: "SENT";
|
|
7
9
|
readonly PAID: "PAID";
|
|
@@ -27,8 +29,9 @@ export interface Bill {
|
|
|
27
29
|
intent_created_at: number;
|
|
28
30
|
intent_last_created_at: number;
|
|
29
31
|
payments: BillPayment[];
|
|
30
|
-
line_items:
|
|
32
|
+
line_items: BillLineItem[];
|
|
31
33
|
prepayments: string[];
|
|
32
34
|
months: string[];
|
|
33
35
|
due_date: number;
|
|
36
|
+
pass_on_fees: boolean;
|
|
34
37
|
}
|
|
@@ -14,6 +14,7 @@ export declare const LineItemType: {
|
|
|
14
14
|
readonly EVENT: "EVENT";
|
|
15
15
|
readonly SERVICE_MONTH: "SERVICE_MONTH";
|
|
16
16
|
readonly PREPAYMENT: "PREPAYMENT";
|
|
17
|
+
readonly PROCESSING_FEE: "PROCESSING_FEE";
|
|
17
18
|
};
|
|
18
19
|
export type LineItemType = (typeof LineItemType)[keyof typeof LineItemType];
|
|
19
20
|
declare module '../../types/api/LineItem' {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LineItem } from '../../types/api/LineItem';
|
|
2
|
+
import { PaymentType } from '../../types/api/PaymentType';
|
|
3
|
+
/**
|
|
4
|
+
* A read-only line item populated by the backend when the bill has
|
|
5
|
+
* `pass_on_fees` enabled. Represents the Stripe + Avro processing fees being
|
|
6
|
+
* passed on to the end customer.
|
|
7
|
+
*
|
|
8
|
+
* Merchants cannot edit, add, or delete these rows directly; they are managed
|
|
9
|
+
* by the server (created on bill save, recomputed when the payment method
|
|
10
|
+
* changes at checkout, and reconciled to the actual collected fees on
|
|
11
|
+
* `payment_intent.succeeded`).
|
|
12
|
+
*
|
|
13
|
+
* `payment_method_assumption` records which method was used to compute the
|
|
14
|
+
* current `cost`. UI surfaces (invoice details, email PDF, QB sync) display
|
|
15
|
+
* this value so the merchant knows whether the persisted figure is the card
|
|
16
|
+
* estimate, the ACH estimate, or the actual reconciled amount.
|
|
17
|
+
*/
|
|
18
|
+
export interface ProcessingFeeLineItem extends LineItem {
|
|
19
|
+
line_item_type: 'PROCESSING_FEE';
|
|
20
|
+
payment_method_assumption: PaymentType | null;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/api.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from '../types/api/PaymentType';
|
|
|
25
25
|
export * from '../types/api/Plan';
|
|
26
26
|
export * from '../types/api/PlanPayment';
|
|
27
27
|
export * from '../types/api/Prepayment';
|
|
28
|
+
export * from '../types/api/ProcessingFeeLineItem';
|
|
28
29
|
export * from '../types/api/Reaction';
|
|
29
30
|
export * from '../types/api/Route';
|
|
30
31
|
export * from '../types/api/RouteJob';
|
package/dist/types/api.js
CHANGED
|
@@ -24,6 +24,7 @@ export * from '../types/api/PaymentType';
|
|
|
24
24
|
export * from '../types/api/Plan';
|
|
25
25
|
export * from '../types/api/PlanPayment';
|
|
26
26
|
export * from '../types/api/Prepayment';
|
|
27
|
+
export * from '../types/api/ProcessingFeeLineItem';
|
|
27
28
|
export * from '../types/api/Reaction';
|
|
28
29
|
export * from '../types/api/Route';
|
|
29
30
|
export * from '../types/api/RouteJob';
|