@hanzo/commerce.js 1.0.0
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 +62 -0
- package/dist/billing.d.mts +163 -0
- package/dist/billing.d.ts +163 -0
- package/dist/billing.js +200 -0
- package/dist/billing.mjs +174 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/index.mjs +2 -0
- package/dist/types.d.mts +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +24 -0
- package/dist/types.mjs +2 -0
- package/package.json +77 -0
- package/src/billing.ts +328 -0
- package/src/index.ts +23 -0
- package/src/types.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @hanzo/commerce.js
|
|
2
|
+
|
|
3
|
+
Modern e-commerce framework for React/Next.js. Wraps @hanzo/commerce with a standalone package.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hanzo/commerce.js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
ProductCard,
|
|
16
|
+
AddToCartWidget,
|
|
17
|
+
CartPanel,
|
|
18
|
+
CommerceProvider
|
|
19
|
+
} from '@hanzo/commerce.js'
|
|
20
|
+
|
|
21
|
+
function Shop({ products }) {
|
|
22
|
+
return (
|
|
23
|
+
<CommerceProvider>
|
|
24
|
+
<div className="products">
|
|
25
|
+
{products.map(product => (
|
|
26
|
+
<ProductCard key={product.id} product={product}>
|
|
27
|
+
<AddToCartWidget sku={product.sku} />
|
|
28
|
+
</ProductCard>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
<CartPanel />
|
|
32
|
+
</CommerceProvider>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Components
|
|
38
|
+
|
|
39
|
+
### Product Display
|
|
40
|
+
- `ProductCard` - Display product information
|
|
41
|
+
- `CarouselBuyCard` - Product card with carousel
|
|
42
|
+
- `Icons` - Commerce icons
|
|
43
|
+
|
|
44
|
+
### Cart
|
|
45
|
+
- `CartPanel` - Shopping cart sidebar
|
|
46
|
+
- `AddToCartWidget` - Add to cart button with quantity
|
|
47
|
+
|
|
48
|
+
### Checkout
|
|
49
|
+
- `PaymentStepForm` - Payment form step
|
|
50
|
+
- `ShippingStepForm` - Shipping form step
|
|
51
|
+
|
|
52
|
+
## Types
|
|
53
|
+
|
|
54
|
+
Import types from the `/types` subpath:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { Product, LineItem, CartItem } from '@hanzo/commerce.js/types'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
BSD-3-Clause
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hanzo/commerce.js/billing
|
|
3
|
+
*
|
|
4
|
+
* THE canonical billing client for the Hanzo platform.
|
|
5
|
+
* Talks to Commerce API (Go backend) for all billing operations:
|
|
6
|
+
* balance, usage, deposits, subscriptions, checkout, plans.
|
|
7
|
+
*
|
|
8
|
+
* One SDK, one way to do billing. All services use this.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { BillingClient } from '@hanzo/commerce.js/billing'
|
|
13
|
+
*
|
|
14
|
+
* const billing = new BillingClient({
|
|
15
|
+
* commerceUrl: 'https://commerce.hanzo.ai',
|
|
16
|
+
* token: iamAccessToken,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // Check balance
|
|
20
|
+
* const balance = await billing.getBalance('hanzo/alice')
|
|
21
|
+
*
|
|
22
|
+
* // Record usage
|
|
23
|
+
* await billing.addUsageRecord({
|
|
24
|
+
* user: 'hanzo/alice',
|
|
25
|
+
* amount: 150,
|
|
26
|
+
* model: 'claude-opus-4-6',
|
|
27
|
+
* tokens: 5000,
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // Subscribe
|
|
31
|
+
* await billing.subscribe({ planId: 'growth', userId: 'hanzo/alice' })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
type CommerceConfig = {
|
|
35
|
+
/** Commerce API base URL (e.g. "https://commerce.hanzo.ai"). */
|
|
36
|
+
commerceUrl: string;
|
|
37
|
+
/** Optional IAM access token for authenticated requests. */
|
|
38
|
+
token?: string;
|
|
39
|
+
};
|
|
40
|
+
type Balance = {
|
|
41
|
+
balance: number;
|
|
42
|
+
holds: number;
|
|
43
|
+
available: number;
|
|
44
|
+
};
|
|
45
|
+
type Transaction = {
|
|
46
|
+
id?: string;
|
|
47
|
+
owner?: string;
|
|
48
|
+
type: 'hold' | 'hold-removed' | 'transfer' | 'deposit' | 'withdraw';
|
|
49
|
+
destinationId?: string;
|
|
50
|
+
destinationKind?: string;
|
|
51
|
+
sourceId?: string;
|
|
52
|
+
sourceKind?: string;
|
|
53
|
+
currency: string;
|
|
54
|
+
amount: number;
|
|
55
|
+
tags?: string[];
|
|
56
|
+
expiresAt?: string;
|
|
57
|
+
metadata?: Record<string, unknown>;
|
|
58
|
+
createdAt?: string;
|
|
59
|
+
};
|
|
60
|
+
type Subscription = {
|
|
61
|
+
id?: string;
|
|
62
|
+
planId?: string;
|
|
63
|
+
userId?: string;
|
|
64
|
+
status?: 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | string;
|
|
65
|
+
billingType?: 'charge_automatically' | 'send_invoice';
|
|
66
|
+
periodStart?: string;
|
|
67
|
+
periodEnd?: string;
|
|
68
|
+
trialStart?: string;
|
|
69
|
+
trialEnd?: string;
|
|
70
|
+
quantity?: number;
|
|
71
|
+
createdAt?: string;
|
|
72
|
+
};
|
|
73
|
+
type Plan = {
|
|
74
|
+
slug?: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
price?: number;
|
|
78
|
+
currency?: string;
|
|
79
|
+
interval?: 'monthly' | 'yearly' | string;
|
|
80
|
+
intervalCount?: number;
|
|
81
|
+
trialPeriodDays?: number;
|
|
82
|
+
metadata?: Record<string, unknown>;
|
|
83
|
+
};
|
|
84
|
+
type Payment = {
|
|
85
|
+
id?: string;
|
|
86
|
+
orderId?: string;
|
|
87
|
+
userId?: string;
|
|
88
|
+
amount?: number;
|
|
89
|
+
amountRefunded?: number;
|
|
90
|
+
fee?: number;
|
|
91
|
+
currency?: string;
|
|
92
|
+
status?: 'cancelled' | 'credit' | 'disputed' | 'failed' | 'fraudulent' | 'paid' | 'refunded' | 'unpaid' | string;
|
|
93
|
+
captured?: boolean;
|
|
94
|
+
live?: boolean;
|
|
95
|
+
createdAt?: string;
|
|
96
|
+
};
|
|
97
|
+
type UsageRecord = {
|
|
98
|
+
user: string;
|
|
99
|
+
currency?: string;
|
|
100
|
+
amount: number;
|
|
101
|
+
model?: string;
|
|
102
|
+
provider?: string;
|
|
103
|
+
tokens?: number;
|
|
104
|
+
promptTokens?: number;
|
|
105
|
+
completionTokens?: number;
|
|
106
|
+
};
|
|
107
|
+
declare class BillingClient {
|
|
108
|
+
private readonly baseUrl;
|
|
109
|
+
private token;
|
|
110
|
+
constructor(config: CommerceConfig);
|
|
111
|
+
/** Update the auth token (e.g. after IAM token refresh). */
|
|
112
|
+
setToken(token: string): void;
|
|
113
|
+
private request;
|
|
114
|
+
/** Get balance for a user in a specific currency. */
|
|
115
|
+
getBalance(user: string, currency?: string, token?: string): Promise<Balance>;
|
|
116
|
+
/** Get balances across all currencies for a user. */
|
|
117
|
+
getAllBalances(user: string, token?: string): Promise<Record<string, Balance>>;
|
|
118
|
+
/** Record API usage (creates a withdraw transaction). */
|
|
119
|
+
addUsageRecord(record: UsageRecord, token?: string): Promise<Transaction>;
|
|
120
|
+
/** Get usage transactions for a user. */
|
|
121
|
+
getUsageRecords(user: string, currency?: string, token?: string): Promise<Transaction[]>;
|
|
122
|
+
/** Create a deposit (credit) for a user. */
|
|
123
|
+
addDeposit(params: {
|
|
124
|
+
user: string;
|
|
125
|
+
currency?: string;
|
|
126
|
+
amount: number;
|
|
127
|
+
notes?: string;
|
|
128
|
+
tags?: string[];
|
|
129
|
+
expiresIn?: string;
|
|
130
|
+
}, token?: string): Promise<Transaction>;
|
|
131
|
+
/** Grant starter credit ($5 USD, 30-day expiry). */
|
|
132
|
+
grantStarterCredit(user: string, token?: string): Promise<Transaction>;
|
|
133
|
+
/** Create a new subscription. */
|
|
134
|
+
subscribe(params: {
|
|
135
|
+
planId: string;
|
|
136
|
+
userId: string;
|
|
137
|
+
paymentToken?: string;
|
|
138
|
+
}, token?: string): Promise<Subscription>;
|
|
139
|
+
/** Get a subscription by ID. */
|
|
140
|
+
getSubscription(subscriptionId: string, token?: string): Promise<Subscription | null>;
|
|
141
|
+
/** Update a subscription. */
|
|
142
|
+
updateSubscription(subscriptionId: string, update: Partial<Subscription>, token?: string): Promise<Subscription>;
|
|
143
|
+
/** Cancel a subscription. */
|
|
144
|
+
cancelSubscription(subscriptionId: string, token?: string): Promise<void>;
|
|
145
|
+
/** Authorize a payment for an order. */
|
|
146
|
+
authorize(orderId: string, token?: string): Promise<Payment>;
|
|
147
|
+
/** Capture a previously authorized payment. */
|
|
148
|
+
capture(orderId: string, token?: string): Promise<Payment>;
|
|
149
|
+
/** Authorize and capture (full charge). */
|
|
150
|
+
charge(orderId: string, token?: string): Promise<Payment>;
|
|
151
|
+
/** Refund a payment. */
|
|
152
|
+
refund(paymentId: string, token?: string): Promise<Payment>;
|
|
153
|
+
/** Get available plans. */
|
|
154
|
+
getPlans(token?: string): Promise<Plan[]>;
|
|
155
|
+
/** Get a specific plan by slug/ID. */
|
|
156
|
+
getPlan(planId: string, token?: string): Promise<Plan | null>;
|
|
157
|
+
}
|
|
158
|
+
declare class CommerceApiError extends Error {
|
|
159
|
+
readonly status: number;
|
|
160
|
+
constructor(status: number, message: string);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { type Balance, BillingClient, CommerceApiError, type CommerceConfig, type Payment, type Plan, type Subscription, type Transaction, type UsageRecord };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hanzo/commerce.js/billing
|
|
3
|
+
*
|
|
4
|
+
* THE canonical billing client for the Hanzo platform.
|
|
5
|
+
* Talks to Commerce API (Go backend) for all billing operations:
|
|
6
|
+
* balance, usage, deposits, subscriptions, checkout, plans.
|
|
7
|
+
*
|
|
8
|
+
* One SDK, one way to do billing. All services use this.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { BillingClient } from '@hanzo/commerce.js/billing'
|
|
13
|
+
*
|
|
14
|
+
* const billing = new BillingClient({
|
|
15
|
+
* commerceUrl: 'https://commerce.hanzo.ai',
|
|
16
|
+
* token: iamAccessToken,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // Check balance
|
|
20
|
+
* const balance = await billing.getBalance('hanzo/alice')
|
|
21
|
+
*
|
|
22
|
+
* // Record usage
|
|
23
|
+
* await billing.addUsageRecord({
|
|
24
|
+
* user: 'hanzo/alice',
|
|
25
|
+
* amount: 150,
|
|
26
|
+
* model: 'claude-opus-4-6',
|
|
27
|
+
* tokens: 5000,
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // Subscribe
|
|
31
|
+
* await billing.subscribe({ planId: 'growth', userId: 'hanzo/alice' })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
type CommerceConfig = {
|
|
35
|
+
/** Commerce API base URL (e.g. "https://commerce.hanzo.ai"). */
|
|
36
|
+
commerceUrl: string;
|
|
37
|
+
/** Optional IAM access token for authenticated requests. */
|
|
38
|
+
token?: string;
|
|
39
|
+
};
|
|
40
|
+
type Balance = {
|
|
41
|
+
balance: number;
|
|
42
|
+
holds: number;
|
|
43
|
+
available: number;
|
|
44
|
+
};
|
|
45
|
+
type Transaction = {
|
|
46
|
+
id?: string;
|
|
47
|
+
owner?: string;
|
|
48
|
+
type: 'hold' | 'hold-removed' | 'transfer' | 'deposit' | 'withdraw';
|
|
49
|
+
destinationId?: string;
|
|
50
|
+
destinationKind?: string;
|
|
51
|
+
sourceId?: string;
|
|
52
|
+
sourceKind?: string;
|
|
53
|
+
currency: string;
|
|
54
|
+
amount: number;
|
|
55
|
+
tags?: string[];
|
|
56
|
+
expiresAt?: string;
|
|
57
|
+
metadata?: Record<string, unknown>;
|
|
58
|
+
createdAt?: string;
|
|
59
|
+
};
|
|
60
|
+
type Subscription = {
|
|
61
|
+
id?: string;
|
|
62
|
+
planId?: string;
|
|
63
|
+
userId?: string;
|
|
64
|
+
status?: 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | string;
|
|
65
|
+
billingType?: 'charge_automatically' | 'send_invoice';
|
|
66
|
+
periodStart?: string;
|
|
67
|
+
periodEnd?: string;
|
|
68
|
+
trialStart?: string;
|
|
69
|
+
trialEnd?: string;
|
|
70
|
+
quantity?: number;
|
|
71
|
+
createdAt?: string;
|
|
72
|
+
};
|
|
73
|
+
type Plan = {
|
|
74
|
+
slug?: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
price?: number;
|
|
78
|
+
currency?: string;
|
|
79
|
+
interval?: 'monthly' | 'yearly' | string;
|
|
80
|
+
intervalCount?: number;
|
|
81
|
+
trialPeriodDays?: number;
|
|
82
|
+
metadata?: Record<string, unknown>;
|
|
83
|
+
};
|
|
84
|
+
type Payment = {
|
|
85
|
+
id?: string;
|
|
86
|
+
orderId?: string;
|
|
87
|
+
userId?: string;
|
|
88
|
+
amount?: number;
|
|
89
|
+
amountRefunded?: number;
|
|
90
|
+
fee?: number;
|
|
91
|
+
currency?: string;
|
|
92
|
+
status?: 'cancelled' | 'credit' | 'disputed' | 'failed' | 'fraudulent' | 'paid' | 'refunded' | 'unpaid' | string;
|
|
93
|
+
captured?: boolean;
|
|
94
|
+
live?: boolean;
|
|
95
|
+
createdAt?: string;
|
|
96
|
+
};
|
|
97
|
+
type UsageRecord = {
|
|
98
|
+
user: string;
|
|
99
|
+
currency?: string;
|
|
100
|
+
amount: number;
|
|
101
|
+
model?: string;
|
|
102
|
+
provider?: string;
|
|
103
|
+
tokens?: number;
|
|
104
|
+
promptTokens?: number;
|
|
105
|
+
completionTokens?: number;
|
|
106
|
+
};
|
|
107
|
+
declare class BillingClient {
|
|
108
|
+
private readonly baseUrl;
|
|
109
|
+
private token;
|
|
110
|
+
constructor(config: CommerceConfig);
|
|
111
|
+
/** Update the auth token (e.g. after IAM token refresh). */
|
|
112
|
+
setToken(token: string): void;
|
|
113
|
+
private request;
|
|
114
|
+
/** Get balance for a user in a specific currency. */
|
|
115
|
+
getBalance(user: string, currency?: string, token?: string): Promise<Balance>;
|
|
116
|
+
/** Get balances across all currencies for a user. */
|
|
117
|
+
getAllBalances(user: string, token?: string): Promise<Record<string, Balance>>;
|
|
118
|
+
/** Record API usage (creates a withdraw transaction). */
|
|
119
|
+
addUsageRecord(record: UsageRecord, token?: string): Promise<Transaction>;
|
|
120
|
+
/** Get usage transactions for a user. */
|
|
121
|
+
getUsageRecords(user: string, currency?: string, token?: string): Promise<Transaction[]>;
|
|
122
|
+
/** Create a deposit (credit) for a user. */
|
|
123
|
+
addDeposit(params: {
|
|
124
|
+
user: string;
|
|
125
|
+
currency?: string;
|
|
126
|
+
amount: number;
|
|
127
|
+
notes?: string;
|
|
128
|
+
tags?: string[];
|
|
129
|
+
expiresIn?: string;
|
|
130
|
+
}, token?: string): Promise<Transaction>;
|
|
131
|
+
/** Grant starter credit ($5 USD, 30-day expiry). */
|
|
132
|
+
grantStarterCredit(user: string, token?: string): Promise<Transaction>;
|
|
133
|
+
/** Create a new subscription. */
|
|
134
|
+
subscribe(params: {
|
|
135
|
+
planId: string;
|
|
136
|
+
userId: string;
|
|
137
|
+
paymentToken?: string;
|
|
138
|
+
}, token?: string): Promise<Subscription>;
|
|
139
|
+
/** Get a subscription by ID. */
|
|
140
|
+
getSubscription(subscriptionId: string, token?: string): Promise<Subscription | null>;
|
|
141
|
+
/** Update a subscription. */
|
|
142
|
+
updateSubscription(subscriptionId: string, update: Partial<Subscription>, token?: string): Promise<Subscription>;
|
|
143
|
+
/** Cancel a subscription. */
|
|
144
|
+
cancelSubscription(subscriptionId: string, token?: string): Promise<void>;
|
|
145
|
+
/** Authorize a payment for an order. */
|
|
146
|
+
authorize(orderId: string, token?: string): Promise<Payment>;
|
|
147
|
+
/** Capture a previously authorized payment. */
|
|
148
|
+
capture(orderId: string, token?: string): Promise<Payment>;
|
|
149
|
+
/** Authorize and capture (full charge). */
|
|
150
|
+
charge(orderId: string, token?: string): Promise<Payment>;
|
|
151
|
+
/** Refund a payment. */
|
|
152
|
+
refund(paymentId: string, token?: string): Promise<Payment>;
|
|
153
|
+
/** Get available plans. */
|
|
154
|
+
getPlans(token?: string): Promise<Plan[]>;
|
|
155
|
+
/** Get a specific plan by slug/ID. */
|
|
156
|
+
getPlan(planId: string, token?: string): Promise<Plan | null>;
|
|
157
|
+
}
|
|
158
|
+
declare class CommerceApiError extends Error {
|
|
159
|
+
readonly status: number;
|
|
160
|
+
constructor(status: number, message: string);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { type Balance, BillingClient, CommerceApiError, type CommerceConfig, type Payment, type Plan, type Subscription, type Transaction, type UsageRecord };
|
package/dist/billing.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/billing.ts
|
|
21
|
+
var billing_exports = {};
|
|
22
|
+
__export(billing_exports, {
|
|
23
|
+
BillingClient: () => BillingClient,
|
|
24
|
+
CommerceApiError: () => CommerceApiError
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(billing_exports);
|
|
27
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
28
|
+
var BillingClient = class {
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.baseUrl = config.commerceUrl.replace(/\/+$/, "");
|
|
31
|
+
this.token = config.token;
|
|
32
|
+
}
|
|
33
|
+
/** Update the auth token (e.g. after IAM token refresh). */
|
|
34
|
+
setToken(token) {
|
|
35
|
+
this.token = token;
|
|
36
|
+
}
|
|
37
|
+
async request(path, opts) {
|
|
38
|
+
const url = new URL(path, this.baseUrl);
|
|
39
|
+
if (opts?.params) {
|
|
40
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
41
|
+
url.searchParams.set(k, v);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
46
|
+
const headers = { Accept: "application/json" };
|
|
47
|
+
const authToken = opts?.token ?? this.token;
|
|
48
|
+
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
49
|
+
if (opts?.body) headers["Content-Type"] = "application/json";
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(url.toString(), {
|
|
52
|
+
method: opts?.method ?? "GET",
|
|
53
|
+
headers,
|
|
54
|
+
body: opts?.body ? JSON.stringify(opts.body) : void 0,
|
|
55
|
+
signal: controller.signal
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const text = await res.text().catch(() => "");
|
|
59
|
+
throw new CommerceApiError(res.status, `${res.statusText}: ${text}`.trim());
|
|
60
|
+
}
|
|
61
|
+
return await res.json();
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// -----------------------------------------------------------------------
|
|
67
|
+
// Balance
|
|
68
|
+
// -----------------------------------------------------------------------
|
|
69
|
+
/** Get balance for a user in a specific currency. */
|
|
70
|
+
async getBalance(user, currency = "usd", token) {
|
|
71
|
+
return this.request("/api/v1/billing/balance", {
|
|
72
|
+
params: { user, currency },
|
|
73
|
+
token
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Get balances across all currencies for a user. */
|
|
77
|
+
async getAllBalances(user, token) {
|
|
78
|
+
return this.request("/api/v1/billing/balance/all", {
|
|
79
|
+
params: { user },
|
|
80
|
+
token
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// -----------------------------------------------------------------------
|
|
84
|
+
// Usage
|
|
85
|
+
// -----------------------------------------------------------------------
|
|
86
|
+
/** Record API usage (creates a withdraw transaction). */
|
|
87
|
+
async addUsageRecord(record, token) {
|
|
88
|
+
return this.request("/api/v1/billing/usage", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: record,
|
|
91
|
+
token
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Get usage transactions for a user. */
|
|
95
|
+
async getUsageRecords(user, currency = "usd", token) {
|
|
96
|
+
return this.request("/api/v1/billing/usage", {
|
|
97
|
+
params: { user, currency },
|
|
98
|
+
token
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// -----------------------------------------------------------------------
|
|
102
|
+
// Deposits / Credits
|
|
103
|
+
// -----------------------------------------------------------------------
|
|
104
|
+
/** Create a deposit (credit) for a user. */
|
|
105
|
+
async addDeposit(params, token) {
|
|
106
|
+
return this.request("/api/v1/billing/deposit", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: params,
|
|
109
|
+
token
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Grant starter credit ($5 USD, 30-day expiry). */
|
|
113
|
+
async grantStarterCredit(user, token) {
|
|
114
|
+
return this.request("/api/v1/billing/credit", {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: { user },
|
|
117
|
+
token
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// -----------------------------------------------------------------------
|
|
121
|
+
// Subscriptions
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
/** Create a new subscription. */
|
|
124
|
+
async subscribe(params, token) {
|
|
125
|
+
return this.request("/api/v1/subscribe", {
|
|
126
|
+
method: "POST",
|
|
127
|
+
body: params,
|
|
128
|
+
token
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Get a subscription by ID. */
|
|
132
|
+
async getSubscription(subscriptionId, token) {
|
|
133
|
+
try {
|
|
134
|
+
return await this.request(`/api/v1/subscribe/${subscriptionId}`, { token });
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Update a subscription. */
|
|
140
|
+
async updateSubscription(subscriptionId, update, token) {
|
|
141
|
+
return this.request(`/api/v1/subscribe/${subscriptionId}`, {
|
|
142
|
+
method: "PATCH",
|
|
143
|
+
body: update,
|
|
144
|
+
token
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Cancel a subscription. */
|
|
148
|
+
async cancelSubscription(subscriptionId, token) {
|
|
149
|
+
await this.request(`/api/v1/subscribe/${subscriptionId}`, {
|
|
150
|
+
method: "DELETE",
|
|
151
|
+
token
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// -----------------------------------------------------------------------
|
|
155
|
+
// Checkout
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
/** Authorize a payment for an order. */
|
|
158
|
+
async authorize(orderId, token) {
|
|
159
|
+
return this.request(`/api/v1/authorize/${orderId}`, { method: "POST", token });
|
|
160
|
+
}
|
|
161
|
+
/** Capture a previously authorized payment. */
|
|
162
|
+
async capture(orderId, token) {
|
|
163
|
+
return this.request(`/api/v1/capture/${orderId}`, { method: "POST", token });
|
|
164
|
+
}
|
|
165
|
+
/** Authorize and capture (full charge). */
|
|
166
|
+
async charge(orderId, token) {
|
|
167
|
+
return this.request(`/api/v1/charge/${orderId}`, { method: "POST", token });
|
|
168
|
+
}
|
|
169
|
+
/** Refund a payment. */
|
|
170
|
+
async refund(paymentId, token) {
|
|
171
|
+
return this.request(`/api/v1/refund/${paymentId}`, { method: "POST", token });
|
|
172
|
+
}
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
// Plans
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
/** Get available plans. */
|
|
177
|
+
async getPlans(token) {
|
|
178
|
+
return this.request("/api/v1/plan", { token });
|
|
179
|
+
}
|
|
180
|
+
/** Get a specific plan by slug/ID. */
|
|
181
|
+
async getPlan(planId, token) {
|
|
182
|
+
try {
|
|
183
|
+
return await this.request(`/api/v1/plan/${planId}`, { token });
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var CommerceApiError = class extends Error {
|
|
190
|
+
constructor(status, message) {
|
|
191
|
+
super(message);
|
|
192
|
+
this.name = "CommerceApiError";
|
|
193
|
+
this.status = status;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
197
|
+
0 && (module.exports = {
|
|
198
|
+
BillingClient,
|
|
199
|
+
CommerceApiError
|
|
200
|
+
});
|
package/dist/billing.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/billing.ts
|
|
2
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
3
|
+
var BillingClient = class {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.baseUrl = config.commerceUrl.replace(/\/+$/, "");
|
|
6
|
+
this.token = config.token;
|
|
7
|
+
}
|
|
8
|
+
/** Update the auth token (e.g. after IAM token refresh). */
|
|
9
|
+
setToken(token) {
|
|
10
|
+
this.token = token;
|
|
11
|
+
}
|
|
12
|
+
async request(path, opts) {
|
|
13
|
+
const url = new URL(path, this.baseUrl);
|
|
14
|
+
if (opts?.params) {
|
|
15
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
16
|
+
url.searchParams.set(k, v);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
21
|
+
const headers = { Accept: "application/json" };
|
|
22
|
+
const authToken = opts?.token ?? this.token;
|
|
23
|
+
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
24
|
+
if (opts?.body) headers["Content-Type"] = "application/json";
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(url.toString(), {
|
|
27
|
+
method: opts?.method ?? "GET",
|
|
28
|
+
headers,
|
|
29
|
+
body: opts?.body ? JSON.stringify(opts.body) : void 0,
|
|
30
|
+
signal: controller.signal
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text().catch(() => "");
|
|
34
|
+
throw new CommerceApiError(res.status, `${res.statusText}: ${text}`.trim());
|
|
35
|
+
}
|
|
36
|
+
return await res.json();
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// -----------------------------------------------------------------------
|
|
42
|
+
// Balance
|
|
43
|
+
// -----------------------------------------------------------------------
|
|
44
|
+
/** Get balance for a user in a specific currency. */
|
|
45
|
+
async getBalance(user, currency = "usd", token) {
|
|
46
|
+
return this.request("/api/v1/billing/balance", {
|
|
47
|
+
params: { user, currency },
|
|
48
|
+
token
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Get balances across all currencies for a user. */
|
|
52
|
+
async getAllBalances(user, token) {
|
|
53
|
+
return this.request("/api/v1/billing/balance/all", {
|
|
54
|
+
params: { user },
|
|
55
|
+
token
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// -----------------------------------------------------------------------
|
|
59
|
+
// Usage
|
|
60
|
+
// -----------------------------------------------------------------------
|
|
61
|
+
/** Record API usage (creates a withdraw transaction). */
|
|
62
|
+
async addUsageRecord(record, token) {
|
|
63
|
+
return this.request("/api/v1/billing/usage", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: record,
|
|
66
|
+
token
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Get usage transactions for a user. */
|
|
70
|
+
async getUsageRecords(user, currency = "usd", token) {
|
|
71
|
+
return this.request("/api/v1/billing/usage", {
|
|
72
|
+
params: { user, currency },
|
|
73
|
+
token
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// -----------------------------------------------------------------------
|
|
77
|
+
// Deposits / Credits
|
|
78
|
+
// -----------------------------------------------------------------------
|
|
79
|
+
/** Create a deposit (credit) for a user. */
|
|
80
|
+
async addDeposit(params, token) {
|
|
81
|
+
return this.request("/api/v1/billing/deposit", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: params,
|
|
84
|
+
token
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Grant starter credit ($5 USD, 30-day expiry). */
|
|
88
|
+
async grantStarterCredit(user, token) {
|
|
89
|
+
return this.request("/api/v1/billing/credit", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: { user },
|
|
92
|
+
token
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// -----------------------------------------------------------------------
|
|
96
|
+
// Subscriptions
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
/** Create a new subscription. */
|
|
99
|
+
async subscribe(params, token) {
|
|
100
|
+
return this.request("/api/v1/subscribe", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: params,
|
|
103
|
+
token
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Get a subscription by ID. */
|
|
107
|
+
async getSubscription(subscriptionId, token) {
|
|
108
|
+
try {
|
|
109
|
+
return await this.request(`/api/v1/subscribe/${subscriptionId}`, { token });
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Update a subscription. */
|
|
115
|
+
async updateSubscription(subscriptionId, update, token) {
|
|
116
|
+
return this.request(`/api/v1/subscribe/${subscriptionId}`, {
|
|
117
|
+
method: "PATCH",
|
|
118
|
+
body: update,
|
|
119
|
+
token
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Cancel a subscription. */
|
|
123
|
+
async cancelSubscription(subscriptionId, token) {
|
|
124
|
+
await this.request(`/api/v1/subscribe/${subscriptionId}`, {
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
token
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// -----------------------------------------------------------------------
|
|
130
|
+
// Checkout
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
/** Authorize a payment for an order. */
|
|
133
|
+
async authorize(orderId, token) {
|
|
134
|
+
return this.request(`/api/v1/authorize/${orderId}`, { method: "POST", token });
|
|
135
|
+
}
|
|
136
|
+
/** Capture a previously authorized payment. */
|
|
137
|
+
async capture(orderId, token) {
|
|
138
|
+
return this.request(`/api/v1/capture/${orderId}`, { method: "POST", token });
|
|
139
|
+
}
|
|
140
|
+
/** Authorize and capture (full charge). */
|
|
141
|
+
async charge(orderId, token) {
|
|
142
|
+
return this.request(`/api/v1/charge/${orderId}`, { method: "POST", token });
|
|
143
|
+
}
|
|
144
|
+
/** Refund a payment. */
|
|
145
|
+
async refund(paymentId, token) {
|
|
146
|
+
return this.request(`/api/v1/refund/${paymentId}`, { method: "POST", token });
|
|
147
|
+
}
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
// Plans
|
|
150
|
+
// -----------------------------------------------------------------------
|
|
151
|
+
/** Get available plans. */
|
|
152
|
+
async getPlans(token) {
|
|
153
|
+
return this.request("/api/v1/plan", { token });
|
|
154
|
+
}
|
|
155
|
+
/** Get a specific plan by slug/ID. */
|
|
156
|
+
async getPlan(planId, token) {
|
|
157
|
+
try {
|
|
158
|
+
return await this.request(`/api/v1/plan/${planId}`, { token });
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var CommerceApiError = class extends Error {
|
|
165
|
+
constructor(status, message) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "CommerceApiError";
|
|
168
|
+
this.status = status;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
export {
|
|
172
|
+
BillingClient,
|
|
173
|
+
CommerceApiError
|
|
174
|
+
};
|
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
15
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
16
|
+
|
|
17
|
+
// src/index.ts
|
|
18
|
+
var index_exports = {};
|
|
19
|
+
module.exports = __toCommonJS(index_exports);
|
|
20
|
+
__reExport(index_exports, require("@hanzo/commerce"), module.exports);
|
|
21
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
22
|
+
0 && (module.exports = {
|
|
23
|
+
...require("@hanzo/commerce")
|
|
24
|
+
});
|
package/dist/index.mjs
ADDED
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@hanzo/commerce/types';
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@hanzo/commerce/types';
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
15
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
16
|
+
|
|
17
|
+
// src/types.ts
|
|
18
|
+
var types_exports = {};
|
|
19
|
+
module.exports = __toCommonJS(types_exports);
|
|
20
|
+
__reExport(types_exports, require("@hanzo/commerce/types"), module.exports);
|
|
21
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
22
|
+
0 && (module.exports = {
|
|
23
|
+
...require("@hanzo/commerce/types")
|
|
24
|
+
});
|
package/dist/types.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanzo/commerce.js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Hanzo Commerce SDK - Modern e-commerce framework. Wraps @hanzo/commerce.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org/",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"module": "dist/index.mjs",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./types": {
|
|
19
|
+
"import": "./dist/types.mjs",
|
|
20
|
+
"require": "./dist/types.js",
|
|
21
|
+
"types": "./dist/types.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./billing": {
|
|
24
|
+
"import": "./dist/billing.mjs",
|
|
25
|
+
"require": "./dist/billing.js",
|
|
26
|
+
"types": "./dist/billing.d.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup src/index.ts src/types.ts src/billing.ts --format cjs,esm --dts --clean",
|
|
35
|
+
"dev": "tsup src/index.ts src/types.ts src/billing.ts --format cjs,esm --dts --watch",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/hanzo-js/commerce.js.git"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"commerce",
|
|
45
|
+
"ecommerce",
|
|
46
|
+
"cart",
|
|
47
|
+
"checkout",
|
|
48
|
+
"payments",
|
|
49
|
+
"hanzo",
|
|
50
|
+
"react",
|
|
51
|
+
"nextjs"
|
|
52
|
+
],
|
|
53
|
+
"author": "Hanzo AI, Inc. <hi@hanzo.ai>",
|
|
54
|
+
"license": "BSD-3-Clause",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/hanzo-js/commerce.js/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/hanzo-js/commerce.js#readme",
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@hanzo/commerce": "^7.3.10"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/react": "^19.0.0",
|
|
64
|
+
"react": "^19.0.0",
|
|
65
|
+
"tsup": "^8.0.0",
|
|
66
|
+
"typescript": "^5.3.0"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
70
|
+
"next": ">=14.0.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"next": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/billing.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hanzo/commerce.js/billing
|
|
3
|
+
*
|
|
4
|
+
* THE canonical billing client for the Hanzo platform.
|
|
5
|
+
* Talks to Commerce API (Go backend) for all billing operations:
|
|
6
|
+
* balance, usage, deposits, subscriptions, checkout, plans.
|
|
7
|
+
*
|
|
8
|
+
* One SDK, one way to do billing. All services use this.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { BillingClient } from '@hanzo/commerce.js/billing'
|
|
13
|
+
*
|
|
14
|
+
* const billing = new BillingClient({
|
|
15
|
+
* commerceUrl: 'https://commerce.hanzo.ai',
|
|
16
|
+
* token: iamAccessToken,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // Check balance
|
|
20
|
+
* const balance = await billing.getBalance('hanzo/alice')
|
|
21
|
+
*
|
|
22
|
+
* // Record usage
|
|
23
|
+
* await billing.addUsageRecord({
|
|
24
|
+
* user: 'hanzo/alice',
|
|
25
|
+
* amount: 150,
|
|
26
|
+
* model: 'claude-opus-4-6',
|
|
27
|
+
* tokens: 5000,
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // Subscribe
|
|
31
|
+
* await billing.subscribe({ planId: 'growth', userId: 'hanzo/alice' })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Config
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export type CommerceConfig = {
|
|
40
|
+
/** Commerce API base URL (e.g. "https://commerce.hanzo.ai"). */
|
|
41
|
+
commerceUrl: string
|
|
42
|
+
/** Optional IAM access token for authenticated requests. */
|
|
43
|
+
token?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Types
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export type Balance = {
|
|
51
|
+
balance: number
|
|
52
|
+
holds: number
|
|
53
|
+
available: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type Transaction = {
|
|
57
|
+
id?: string
|
|
58
|
+
owner?: string
|
|
59
|
+
type: 'hold' | 'hold-removed' | 'transfer' | 'deposit' | 'withdraw'
|
|
60
|
+
destinationId?: string
|
|
61
|
+
destinationKind?: string
|
|
62
|
+
sourceId?: string
|
|
63
|
+
sourceKind?: string
|
|
64
|
+
currency: string
|
|
65
|
+
amount: number
|
|
66
|
+
tags?: string[]
|
|
67
|
+
expiresAt?: string
|
|
68
|
+
metadata?: Record<string, unknown>
|
|
69
|
+
createdAt?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type Subscription = {
|
|
73
|
+
id?: string
|
|
74
|
+
planId?: string
|
|
75
|
+
userId?: string
|
|
76
|
+
status?: 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | string
|
|
77
|
+
billingType?: 'charge_automatically' | 'send_invoice'
|
|
78
|
+
periodStart?: string
|
|
79
|
+
periodEnd?: string
|
|
80
|
+
trialStart?: string
|
|
81
|
+
trialEnd?: string
|
|
82
|
+
quantity?: number
|
|
83
|
+
createdAt?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type Plan = {
|
|
87
|
+
slug?: string
|
|
88
|
+
name?: string
|
|
89
|
+
description?: string
|
|
90
|
+
price?: number
|
|
91
|
+
currency?: string
|
|
92
|
+
interval?: 'monthly' | 'yearly' | string
|
|
93
|
+
intervalCount?: number
|
|
94
|
+
trialPeriodDays?: number
|
|
95
|
+
metadata?: Record<string, unknown>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type Payment = {
|
|
99
|
+
id?: string
|
|
100
|
+
orderId?: string
|
|
101
|
+
userId?: string
|
|
102
|
+
amount?: number
|
|
103
|
+
amountRefunded?: number
|
|
104
|
+
fee?: number
|
|
105
|
+
currency?: string
|
|
106
|
+
status?: 'cancelled' | 'credit' | 'disputed' | 'failed' | 'fraudulent' | 'paid' | 'refunded' | 'unpaid' | string
|
|
107
|
+
captured?: boolean
|
|
108
|
+
live?: boolean
|
|
109
|
+
createdAt?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type UsageRecord = {
|
|
113
|
+
user: string
|
|
114
|
+
currency?: string
|
|
115
|
+
amount: number
|
|
116
|
+
model?: string
|
|
117
|
+
provider?: string
|
|
118
|
+
tokens?: number
|
|
119
|
+
promptTokens?: number
|
|
120
|
+
completionTokens?: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Client
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const DEFAULT_TIMEOUT_MS = 10_000
|
|
128
|
+
|
|
129
|
+
export class BillingClient {
|
|
130
|
+
private readonly baseUrl: string
|
|
131
|
+
private token: string | undefined
|
|
132
|
+
|
|
133
|
+
constructor(config: CommerceConfig) {
|
|
134
|
+
this.baseUrl = config.commerceUrl.replace(/\/+$/, '')
|
|
135
|
+
this.token = config.token
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Update the auth token (e.g. after IAM token refresh). */
|
|
139
|
+
setToken(token: string) {
|
|
140
|
+
this.token = token
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async request<T>(
|
|
144
|
+
path: string,
|
|
145
|
+
opts?: {
|
|
146
|
+
method?: string
|
|
147
|
+
body?: unknown
|
|
148
|
+
token?: string
|
|
149
|
+
params?: Record<string, string>
|
|
150
|
+
},
|
|
151
|
+
): Promise<T> {
|
|
152
|
+
const url = new URL(path, this.baseUrl)
|
|
153
|
+
if (opts?.params) {
|
|
154
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
155
|
+
url.searchParams.set(k, v)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const controller = new AbortController()
|
|
160
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS)
|
|
161
|
+
|
|
162
|
+
const headers: Record<string, string> = { Accept: 'application/json' }
|
|
163
|
+
const authToken = opts?.token ?? this.token
|
|
164
|
+
if (authToken) headers.Authorization = `Bearer ${authToken}`
|
|
165
|
+
if (opts?.body) headers['Content-Type'] = 'application/json'
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(url.toString(), {
|
|
169
|
+
method: opts?.method ?? 'GET',
|
|
170
|
+
headers,
|
|
171
|
+
body: opts?.body ? JSON.stringify(opts.body) : undefined,
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const text = await res.text().catch(() => '')
|
|
177
|
+
throw new CommerceApiError(res.status, `${res.statusText}: ${text}`.trim())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (await res.json()) as T
|
|
181
|
+
} finally {
|
|
182
|
+
clearTimeout(timer)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
// Balance
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** Get balance for a user in a specific currency. */
|
|
191
|
+
async getBalance(user: string, currency = 'usd', token?: string): Promise<Balance> {
|
|
192
|
+
return this.request<Balance>('/api/v1/billing/balance', {
|
|
193
|
+
params: { user, currency }, token,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Get balances across all currencies for a user. */
|
|
198
|
+
async getAllBalances(user: string, token?: string): Promise<Record<string, Balance>> {
|
|
199
|
+
return this.request<Record<string, Balance>>('/api/v1/billing/balance/all', {
|
|
200
|
+
params: { user }, token,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
// Usage
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/** Record API usage (creates a withdraw transaction). */
|
|
209
|
+
async addUsageRecord(record: UsageRecord, token?: string): Promise<Transaction> {
|
|
210
|
+
return this.request<Transaction>('/api/v1/billing/usage', {
|
|
211
|
+
method: 'POST', body: record, token,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Get usage transactions for a user. */
|
|
216
|
+
async getUsageRecords(user: string, currency = 'usd', token?: string): Promise<Transaction[]> {
|
|
217
|
+
return this.request<Transaction[]>('/api/v1/billing/usage', {
|
|
218
|
+
params: { user, currency }, token,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// -----------------------------------------------------------------------
|
|
223
|
+
// Deposits / Credits
|
|
224
|
+
// -----------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/** Create a deposit (credit) for a user. */
|
|
227
|
+
async addDeposit(
|
|
228
|
+
params: { user: string; currency?: string; amount: number; notes?: string; tags?: string[]; expiresIn?: string },
|
|
229
|
+
token?: string,
|
|
230
|
+
): Promise<Transaction> {
|
|
231
|
+
return this.request<Transaction>('/api/v1/billing/deposit', {
|
|
232
|
+
method: 'POST', body: params, token,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Grant starter credit ($5 USD, 30-day expiry). */
|
|
237
|
+
async grantStarterCredit(user: string, token?: string): Promise<Transaction> {
|
|
238
|
+
return this.request<Transaction>('/api/v1/billing/credit', {
|
|
239
|
+
method: 'POST', body: { user }, token,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// -----------------------------------------------------------------------
|
|
244
|
+
// Subscriptions
|
|
245
|
+
// -----------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/** Create a new subscription. */
|
|
248
|
+
async subscribe(params: { planId: string; userId: string; paymentToken?: string }, token?: string): Promise<Subscription> {
|
|
249
|
+
return this.request<Subscription>('/api/v1/subscribe', {
|
|
250
|
+
method: 'POST', body: params, token,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Get a subscription by ID. */
|
|
255
|
+
async getSubscription(subscriptionId: string, token?: string): Promise<Subscription | null> {
|
|
256
|
+
try {
|
|
257
|
+
return await this.request<Subscription>(`/api/v1/subscribe/${subscriptionId}`, { token })
|
|
258
|
+
} catch { return null }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Update a subscription. */
|
|
262
|
+
async updateSubscription(subscriptionId: string, update: Partial<Subscription>, token?: string): Promise<Subscription> {
|
|
263
|
+
return this.request<Subscription>(`/api/v1/subscribe/${subscriptionId}`, {
|
|
264
|
+
method: 'PATCH', body: update, token,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Cancel a subscription. */
|
|
269
|
+
async cancelSubscription(subscriptionId: string, token?: string): Promise<void> {
|
|
270
|
+
await this.request<void>(`/api/v1/subscribe/${subscriptionId}`, {
|
|
271
|
+
method: 'DELETE', token,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
// Checkout
|
|
277
|
+
// -----------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/** Authorize a payment for an order. */
|
|
280
|
+
async authorize(orderId: string, token?: string): Promise<Payment> {
|
|
281
|
+
return this.request<Payment>(`/api/v1/authorize/${orderId}`, { method: 'POST', token })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Capture a previously authorized payment. */
|
|
285
|
+
async capture(orderId: string, token?: string): Promise<Payment> {
|
|
286
|
+
return this.request<Payment>(`/api/v1/capture/${orderId}`, { method: 'POST', token })
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Authorize and capture (full charge). */
|
|
290
|
+
async charge(orderId: string, token?: string): Promise<Payment> {
|
|
291
|
+
return this.request<Payment>(`/api/v1/charge/${orderId}`, { method: 'POST', token })
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Refund a payment. */
|
|
295
|
+
async refund(paymentId: string, token?: string): Promise<Payment> {
|
|
296
|
+
return this.request<Payment>(`/api/v1/refund/${paymentId}`, { method: 'POST', token })
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// -----------------------------------------------------------------------
|
|
300
|
+
// Plans
|
|
301
|
+
// -----------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/** Get available plans. */
|
|
304
|
+
async getPlans(token?: string): Promise<Plan[]> {
|
|
305
|
+
return this.request<Plan[]>('/api/v1/plan', { token })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Get a specific plan by slug/ID. */
|
|
309
|
+
async getPlan(planId: string, token?: string): Promise<Plan | null> {
|
|
310
|
+
try {
|
|
311
|
+
return await this.request<Plan>(`/api/v1/plan/${planId}`, { token })
|
|
312
|
+
} catch { return null }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Error
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
export class CommerceApiError extends Error {
|
|
321
|
+
readonly status: number
|
|
322
|
+
|
|
323
|
+
constructor(status: number, message: string) {
|
|
324
|
+
super(message)
|
|
325
|
+
this.name = 'CommerceApiError'
|
|
326
|
+
this.status = status
|
|
327
|
+
}
|
|
328
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commerce.js
|
|
3
|
+
*
|
|
4
|
+
* Hanzo Commerce SDK - Modern e-commerce framework for React/Next.js.
|
|
5
|
+
* This package re-exports @hanzo/commerce for easy standalone usage.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { AddToCartWidget, CartPanel, ProductCard } from 'commerce.js'
|
|
10
|
+
*
|
|
11
|
+
* function ProductPage({ product }) {
|
|
12
|
+
* return (
|
|
13
|
+
* <div>
|
|
14
|
+
* <ProductCard product={product} />
|
|
15
|
+
* <AddToCartWidget sku={product.sku} />
|
|
16
|
+
* </div>
|
|
17
|
+
* )
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Re-export everything from @hanzo/commerce
|
|
23
|
+
export * from '@hanzo/commerce'
|