@go-avro/avro-js 0.0.50 → 0.0.51

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.
@@ -2,7 +2,7 @@ import { PaymentType } from '../types/api/PaymentType';
2
2
  export interface PassOnFeeBreakdown {
3
3
  /** Stripe processing fee on the grossed-up amount, in cents. */
4
4
  stripeFee: number;
5
- /** Avro application fee on the net (gross - stripeFee), in cents. */
5
+ /** Avro application fee on the grossed-up amount, in cents. */
6
6
  avroFee: number;
7
7
  /** Total fee added on top of the subtotal: stripeFee + avroFee. */
8
8
  totalFee: number;
@@ -15,9 +15,13 @@ export interface PassOnFeeBreakdown {
15
15
  */
16
16
  export declare function computeStripeFee(grossCents: number, method: PaymentType): number;
17
17
  /**
18
- * Compute Avro's application fee on a net amount (gross - stripeFee), in cents.
18
+ * Compute Avro's application fee on a gross amount, in cents.
19
+ *
20
+ * Avro's fee is now computed against the gross (matching Stripe's basis)
21
+ * rather than the post-Stripe net. This keeps the pass-on gross-up
22
+ * exactly solvable in closed form.
19
23
  */
20
- export declare function computeAvroFee(netCents: number): number;
24
+ export declare function computeAvroFee(grossCents: number): number;
21
25
  /**
22
26
  * Compute the pass-on fee (in cents) such that, after Stripe and Avro deduct
23
27
  * their fees from the gross amount the customer is charged, the merchant
@@ -28,8 +32,7 @@ export declare function computeAvroFee(netCents: number): number;
28
32
  * Closed form: a finite set of candidate grosses is derived from each fee
29
33
  * regime (avro at the 5¢ floor vs. percentage; ACH stripe at the $5 cap vs.
30
34
  * percentage). The smallest candidate that satisfies the invariant
31
- * `gross - stripeFee(gross) - avroFee(gross - stripeFee(gross)) >= subtotal`
32
- * is selected. No iteration.
35
+ * `gross - stripeFee(gross) - avroFee(gross) >= subtotal` is selected.
33
36
  */
34
37
  export declare function computePassOnFee(subtotalCents: number, method: PaymentType): PassOnFeeBreakdown;
35
38
  /**
@@ -4,20 +4,22 @@ import { PaymentType } from '../types/api/PaymentType';
4
4
  *
5
5
  * When `pass_on_fees` is enabled on a bill, the customer is charged a gross
6
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.
7
+ * the merchant with at least the bill's subtotal. The fee added on top of the
8
+ * subtotal is exposed as a single read-only line item on the bill.
9
9
  *
10
10
  * Stripe US standard pricing (online):
11
11
  * - Card: 2.9% of gross + 30¢
12
12
  * - ACH (us_bank): 0.8% of gross, capped at $5.00
13
13
  * Avro application fee:
14
- * - 0.4% of net (gross - stripe), minimum 5¢
14
+ * - 0.4% of gross, minimum 5¢
15
15
  *
16
16
  * All amounts are integer cents. All math is closed-form (no iteration).
17
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.
18
+ * Because both Stripe and Avro fees are charged against the same base (gross),
19
+ * the gross-up reduces to the standard form `G = (B + fixed) / (1 - rate)`
20
+ * within each regime. The integer-ceiling on each candidate guarantees the
21
+ * merchant nets at least the requested subtotal. In rare regime-boundary cases
22
+ * the merchant may net up to 2¢ more than requested — never less.
21
23
  */
22
24
  // --- Rate constants (per 1000 for integer math where applicable) ---
23
25
  const CARD_PCT_PER_1000 = 29; // 2.9%
@@ -46,12 +48,16 @@ export function computeStripeFee(grossCents, method) {
46
48
  return Math.min(Math.ceil((ACH_PCT_PER_1000 * grossCents) / 1000), ACH_CAP_CENTS);
47
49
  }
48
50
  /**
49
- * Compute Avro's application fee on a net amount (gross - stripeFee), in cents.
51
+ * Compute Avro's application fee on a gross amount, in cents.
52
+ *
53
+ * Avro's fee is now computed against the gross (matching Stripe's basis)
54
+ * rather than the post-Stripe net. This keeps the pass-on gross-up
55
+ * exactly solvable in closed form.
50
56
  */
51
- export function computeAvroFee(netCents) {
52
- if (netCents <= 0)
57
+ export function computeAvroFee(grossCents) {
58
+ if (grossCents <= 0)
53
59
  return AVRO_MIN_CENTS;
54
- return Math.max(Math.ceil((AVRO_PCT_PER_1000 * netCents) / 1000), AVRO_MIN_CENTS);
60
+ return Math.max(Math.ceil((AVRO_PCT_PER_1000 * grossCents) / 1000), AVRO_MIN_CENTS);
55
61
  }
56
62
  /**
57
63
  * Compute the pass-on fee (in cents) such that, after Stripe and Avro deduct
@@ -63,8 +69,7 @@ export function computeAvroFee(netCents) {
63
69
  * Closed form: a finite set of candidate grosses is derived from each fee
64
70
  * regime (avro at the 5¢ floor vs. percentage; ACH stripe at the $5 cap vs.
65
71
  * percentage). The smallest candidate that satisfies the invariant
66
- * `gross - stripeFee(gross) - avroFee(gross - stripeFee(gross)) >= subtotal`
67
- * is selected. No iteration.
72
+ * `gross - stripeFee(gross) - avroFee(gross) >= subtotal` is selected.
68
73
  */
69
74
  export function computePassOnFee(subtotalCents, method) {
70
75
  if (subtotalCents <= 0)
@@ -74,7 +79,7 @@ export function computePassOnFee(subtotalCents, method) {
74
79
  let best = Number.POSITIVE_INFINITY;
75
80
  for (const g of candidates) {
76
81
  const stripe = computeStripeFee(g, method);
77
- const avro = computeAvroFee(g - stripe);
82
+ const avro = computeAvroFee(g);
78
83
  if (g - stripe - avro >= B && g < best) {
79
84
  best = g;
80
85
  }
@@ -85,7 +90,7 @@ export function computePassOnFee(subtotalCents, method) {
85
90
  best = B + Math.ceil(B * 0.05) + CARD_FIXED_CENTS + AVRO_MIN_CENTS;
86
91
  }
87
92
  const stripeFee = computeStripeFee(best, method);
88
- const avroFee = computeAvroFee(best - stripeFee);
93
+ const avroFee = computeAvroFee(best);
89
94
  return {
90
95
  stripeFee,
91
96
  avroFee,
@@ -96,45 +101,42 @@ export function computePassOnFee(subtotalCents, method) {
96
101
  /**
97
102
  * Card regime candidates.
98
103
  *
99
- * Stripe = ceil(0.029 * G) + 30; avro = max(ceil(0.004 * (G - stripe)), 5).
104
+ * stripe = ceil(0.029 * G) + 30; avro = max(ceil(0.004 * G), 5).
100
105
  *
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
106
+ * Both ceil()s can each add up to 1¢ relative to the exact rational form, so
107
+ * each candidate carries a baked-in 2¢ (percentage regime) or 1¢ (floor
103
108
  * regime) safety margin to guarantee the invariant holds for every B in
104
109
  * integer cents — no post-hoc iteration required.
105
110
  *
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
111
+ * 1) Percentage avro: G * 0.967 ≥ B + 30 + 2
112
+ * => G = ceil((1000 * B + 32_000) / 967)
113
+ * 2) Floor avro (5¢): G * 0.971 ≥ B + 30 + 5 + 1
109
114
  * => G = ceil((1000 * B + 36_000) / 971)
110
115
  */
111
116
  function cardCandidates(B) {
112
- return [
113
- Math.ceil((1000000 * B + 31880000) / 967116),
114
- Math.ceil((1000 * B + 36000) / 971),
115
- ];
117
+ return [Math.ceil((1000 * B + 32000) / 967), Math.ceil((1000 * B + 36000) / 971)];
116
118
  }
117
119
  /**
118
120
  * ACH regime candidates.
119
121
  *
120
- * Stripe = min(ceil(0.008 * G), 500); avro = max(ceil(0.004 * (G - stripe)), 5).
122
+ * stripe = min(ceil(0.008 * G), 500); avro = max(ceil(0.004 * G), 5).
121
123
  *
122
124
  * Same ceiling-safety logic as card. Stripe at the $5 cap is a fixed integer
123
125
  * so its ceiling-slack disappears in the capped regimes.
124
126
  *
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
127
+ * 1) No-cap percentage avro: G * 0.988 ≥ B + 2
128
+ * => G = ceil((1000 * (B + 2)) / 988)
129
+ * 2) No-cap floor avro: G * 0.992 ≥ B + 5 + 1
128
130
  * => 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
+ * 3) Capped percentage avro: G * 0.996 ≥ B + 500 + 1
132
+ * => G = ceil((1000 * (B + 501)) / 996)
131
133
  * 4) Capped floor avro: G = B + 505
132
134
  */
133
135
  function achCandidates(B) {
134
136
  return [
135
- Math.ceil((1000000 * (B + 2)) / 988032),
137
+ Math.ceil((1000 * (B + 2)) / 988),
136
138
  Math.ceil((1000 * (B + 6)) / 992),
137
- Math.ceil((1000 * (B + 499)) / 996),
139
+ Math.ceil((1000 * (B + 501)) / 996),
138
140
  B + 505,
139
141
  ];
140
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-avro/avro-js",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "description": "JS client for Avro backend integration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",