@usethrottle/discounts 0.1.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/CLAUDE.md +27 -0
- package/README.md +84 -0
- package/dist/index.cjs +163 -0
- package/dist/index.d.cts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +134 -0
- package/package.json +33 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @usethrottle/discounts
|
|
2
|
+
|
|
3
|
+
Headless public SDK for Throttle discount and promotion code APIs.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
- Server-side TypeScript client only. Do not add browser UI components here.
|
|
8
|
+
- Keep support limited to `percentage` and `fixed_amount`.
|
|
9
|
+
- Preserve `metadata` as an arbitrary JSON object on create/update/get/list.
|
|
10
|
+
- `maxRedemptionsPerCustomer` defaults to `1`; `null` means unlimited.
|
|
11
|
+
- Customer redemption usage is counted on cart-to-order creation, not preview.
|
|
12
|
+
- Preview is non-mutating: `POST /api/v1/discounts/preview`.
|
|
13
|
+
- Cart application is mutating and must remain server-side:
|
|
14
|
+
`POST /api/v1/carts/:cartId/apply-discount` and
|
|
15
|
+
`DELETE /api/v1/carts/:cartId/discount`.
|
|
16
|
+
- Never encourage exposing `THROTTLE_API_KEY` to buyer browsers.
|
|
17
|
+
|
|
18
|
+
## Testing
|
|
19
|
+
|
|
20
|
+
Focused commands:
|
|
21
|
+
|
|
22
|
+
- `pnpm --filter @usethrottle/discounts test`
|
|
23
|
+
- `pnpm --filter @usethrottle/discounts typecheck`
|
|
24
|
+
- `pnpm --filter @usethrottle/discounts build`
|
|
25
|
+
|
|
26
|
+
When endpoint shapes change, update `README.md`, package types, tests, and
|
|
27
|
+
`apps/docs` in the same change.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @usethrottle/discounts
|
|
2
|
+
|
|
3
|
+
Headless TypeScript client for Throttle discount codes.
|
|
4
|
+
|
|
5
|
+
Use it from your server, API routes, server actions, or backend jobs. The SDK
|
|
6
|
+
does not ship checkout UI because promotion code fields need to match each
|
|
7
|
+
storefront's cart and checkout experience.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @usethrottle/discounts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Preview a Code
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { DiscountsClient } from '@usethrottle/discounts';
|
|
19
|
+
|
|
20
|
+
const discounts = new DiscountsClient({
|
|
21
|
+
apiKey: process.env.THROTTLE_API_KEY!,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const preview = await discounts.preview({
|
|
25
|
+
code: 'SUMMER25',
|
|
26
|
+
currency: 'USD',
|
|
27
|
+
subtotal: 10000,
|
|
28
|
+
customerId: 'cust_123',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (preview.valid) {
|
|
32
|
+
console.log(preview.discountTotal); // 2500
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Apply a Code to a Cart
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const cart = await discounts.applyToCart('cart_123', 'SUMMER25');
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Remove a Code from a Cart
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const cart = await discounts.removeFromCart('cart_123');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Create a Discount
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await discounts.create({
|
|
52
|
+
code: 'SUMMER25',
|
|
53
|
+
name: 'Summer 25% off',
|
|
54
|
+
type: 'percentage',
|
|
55
|
+
value: 25,
|
|
56
|
+
conditions: { minOrder: 5000 },
|
|
57
|
+
maxRedemptionsPerCustomer: 1,
|
|
58
|
+
metadata: {
|
|
59
|
+
campaign: 'summer-2026',
|
|
60
|
+
channel: 'email',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await discounts.create({
|
|
65
|
+
code: 'FIVE-OFF',
|
|
66
|
+
name: '$5 off',
|
|
67
|
+
type: 'fixed_amount',
|
|
68
|
+
value: 500,
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Throttle supports only `percentage` and `fixed_amount` discounts. `value` is a
|
|
73
|
+
whole percent for percentage discounts and minor currency units for fixed
|
|
74
|
+
amount discounts.
|
|
75
|
+
|
|
76
|
+
`maxRedemptionsPerCustomer` defaults to `1`. Set it to another integer for
|
|
77
|
+
repeat customer use, or `null` for unlimited customer redemptions. Redemption
|
|
78
|
+
usage is counted when a discounted cart becomes an order.
|
|
79
|
+
|
|
80
|
+
## API Key Safety
|
|
81
|
+
|
|
82
|
+
This package sends `X-API-Key` to the Throttle API. Use it from a trusted server
|
|
83
|
+
environment only. Browser checkout UIs should call your own API route or server
|
|
84
|
+
action, then your server can use this package.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
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/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DiscountsClient: () => DiscountsClient,
|
|
24
|
+
ThrottleDiscountsError: () => ThrottleDiscountsError,
|
|
25
|
+
createDiscountsClient: () => createDiscountsClient
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
var ThrottleDiscountsError = class extends Error {
|
|
31
|
+
code;
|
|
32
|
+
statusCode;
|
|
33
|
+
details;
|
|
34
|
+
constructor(args) {
|
|
35
|
+
super(args.message);
|
|
36
|
+
this.name = "ThrottleDiscountsError";
|
|
37
|
+
this.code = args.code;
|
|
38
|
+
this.statusCode = args.statusCode;
|
|
39
|
+
this.details = args.details;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/client.ts
|
|
44
|
+
var DiscountsClient = class {
|
|
45
|
+
apiKey;
|
|
46
|
+
baseUrl;
|
|
47
|
+
fetchImpl;
|
|
48
|
+
timeoutMs;
|
|
49
|
+
constructor(opts) {
|
|
50
|
+
if (!opts.apiKey) throw new Error("DiscountsClient: apiKey is required");
|
|
51
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
52
|
+
if (!fetchImpl) throw new Error("DiscountsClient: fetch is required");
|
|
53
|
+
this.apiKey = opts.apiKey;
|
|
54
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
55
|
+
this.fetchImpl = fetchImpl;
|
|
56
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
57
|
+
}
|
|
58
|
+
async request(method, path, body) {
|
|
59
|
+
const envelope = await this.requestEnvelope(method, path, body);
|
|
60
|
+
return envelope.data;
|
|
61
|
+
}
|
|
62
|
+
async requestEnvelope(method, path, body) {
|
|
63
|
+
const url = path.startsWith("http") ? path : `${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`;
|
|
64
|
+
const ctrl = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
66
|
+
try {
|
|
67
|
+
const res = await this.fetchImpl(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: {
|
|
70
|
+
"x-api-key": this.apiKey,
|
|
71
|
+
"content-type": "application/json"
|
|
72
|
+
},
|
|
73
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
74
|
+
signal: ctrl.signal
|
|
75
|
+
});
|
|
76
|
+
const json = await res.json().catch(() => ({}));
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new ThrottleDiscountsError({
|
|
79
|
+
code: json?.error?.code ?? "unknown_error",
|
|
80
|
+
message: json?.error?.message ?? `HTTP ${res.status}`,
|
|
81
|
+
statusCode: res.status,
|
|
82
|
+
details: json?.error?.details
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
data: json.data,
|
|
87
|
+
meta: json.meta
|
|
88
|
+
};
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
create(input) {
|
|
94
|
+
return this.request("POST", "/api/v1/discounts", serializeDates(input));
|
|
95
|
+
}
|
|
96
|
+
async list(params = {}) {
|
|
97
|
+
const query = new URLSearchParams();
|
|
98
|
+
if (params.storeId) query.set("storeId", params.storeId);
|
|
99
|
+
if (params.isActive !== void 0) query.set("isActive", String(params.isActive));
|
|
100
|
+
if (params.code) query.set("code", params.code);
|
|
101
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
102
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
103
|
+
if (params.sort) query.set("sort", params.sort);
|
|
104
|
+
const suffix = query.toString() ? `?${query}` : "";
|
|
105
|
+
const envelope = await this.requestEnvelope("GET", `/api/v1/discounts${suffix}`);
|
|
106
|
+
return {
|
|
107
|
+
data: envelope.data,
|
|
108
|
+
pagination: envelope.meta?.pagination ?? void 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
get(id) {
|
|
112
|
+
return this.request("GET", `/api/v1/discounts/${encodeURIComponent(id)}`);
|
|
113
|
+
}
|
|
114
|
+
update(id, input) {
|
|
115
|
+
return this.request(
|
|
116
|
+
"PATCH",
|
|
117
|
+
`/api/v1/discounts/${encodeURIComponent(id)}`,
|
|
118
|
+
serializeDates(input)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
delete(id) {
|
|
122
|
+
return this.request("DELETE", `/api/v1/discounts/${encodeURIComponent(id)}`);
|
|
123
|
+
}
|
|
124
|
+
validate(code, orderTotal, customerId) {
|
|
125
|
+
return this.request("POST", "/api/v1/discounts/validate", {
|
|
126
|
+
code,
|
|
127
|
+
orderTotal,
|
|
128
|
+
...customerId ? { customerId } : {}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
preview(input) {
|
|
132
|
+
return this.request("POST", "/api/v1/discounts/preview", input);
|
|
133
|
+
}
|
|
134
|
+
applyToCart(cartId, code) {
|
|
135
|
+
return this.request(
|
|
136
|
+
"POST",
|
|
137
|
+
`/api/v1/carts/${encodeURIComponent(cartId)}/apply-discount`,
|
|
138
|
+
{
|
|
139
|
+
code
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
removeFromCart(cartId) {
|
|
144
|
+
return this.request("DELETE", `/api/v1/carts/${encodeURIComponent(cartId)}/discount`);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function createDiscountsClient(opts) {
|
|
148
|
+
return new DiscountsClient(opts);
|
|
149
|
+
}
|
|
150
|
+
function serializeDates(input) {
|
|
151
|
+
const out = { ...input };
|
|
152
|
+
for (const key of ["startsAt", "endsAt"]) {
|
|
153
|
+
const value = out[key];
|
|
154
|
+
if (value instanceof Date) out[key] = value.toISOString();
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
159
|
+
0 && (module.exports = {
|
|
160
|
+
DiscountsClient,
|
|
161
|
+
ThrottleDiscountsError,
|
|
162
|
+
createDiscountsClient
|
|
163
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
type DiscountType = 'percentage' | 'fixed_amount';
|
|
2
|
+
interface Discount {
|
|
3
|
+
id: string;
|
|
4
|
+
code: string | null;
|
|
5
|
+
name: string;
|
|
6
|
+
type: DiscountType;
|
|
7
|
+
value: number;
|
|
8
|
+
conditions: Record<string, unknown>;
|
|
9
|
+
usageLimit?: number | null;
|
|
10
|
+
usageCount: number;
|
|
11
|
+
maxRedemptionsPerCustomer?: number | null;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
startsAt?: string | null;
|
|
14
|
+
endsAt?: string | null;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
}
|
|
18
|
+
interface CreateDiscountInput {
|
|
19
|
+
storeId?: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
name: string;
|
|
22
|
+
type: DiscountType;
|
|
23
|
+
value: number;
|
|
24
|
+
conditions?: Record<string, unknown>;
|
|
25
|
+
usageLimit?: number;
|
|
26
|
+
maxRedemptionsPerCustomer?: number | null;
|
|
27
|
+
startsAt?: string | Date;
|
|
28
|
+
endsAt?: string | Date;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface UpdateDiscountInput {
|
|
32
|
+
name?: string;
|
|
33
|
+
value?: number;
|
|
34
|
+
conditions?: Record<string, unknown>;
|
|
35
|
+
usageLimit?: number;
|
|
36
|
+
maxRedemptionsPerCustomer?: number | null;
|
|
37
|
+
isActive?: boolean;
|
|
38
|
+
startsAt?: string | Date;
|
|
39
|
+
endsAt?: string | Date;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
interface DiscountListParams {
|
|
43
|
+
storeId?: string;
|
|
44
|
+
isActive?: boolean;
|
|
45
|
+
code?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
sort?: 'asc' | 'desc';
|
|
49
|
+
}
|
|
50
|
+
interface DiscountValidationResult {
|
|
51
|
+
valid: boolean;
|
|
52
|
+
discount?: Discount;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
interface DiscountPreviewInput {
|
|
56
|
+
code: string;
|
|
57
|
+
currency: string;
|
|
58
|
+
subtotal: number;
|
|
59
|
+
customerId?: string;
|
|
60
|
+
lineItems?: Array<{
|
|
61
|
+
id?: string;
|
|
62
|
+
quantity: number;
|
|
63
|
+
unitPrice: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
interface DiscountPreviewResult {
|
|
67
|
+
valid: boolean;
|
|
68
|
+
code: string;
|
|
69
|
+
type?: DiscountType;
|
|
70
|
+
value?: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
subtotal: number;
|
|
73
|
+
discountTotal: number;
|
|
74
|
+
total: number;
|
|
75
|
+
discount?: Discount;
|
|
76
|
+
message: string | null;
|
|
77
|
+
}
|
|
78
|
+
interface Cart {
|
|
79
|
+
id: string;
|
|
80
|
+
currency: string;
|
|
81
|
+
subtotal: number;
|
|
82
|
+
discountTotal: number;
|
|
83
|
+
shippingTotal: number;
|
|
84
|
+
taxTotal: number;
|
|
85
|
+
total: number;
|
|
86
|
+
discountCode?: string | null;
|
|
87
|
+
appliedDiscount?: {
|
|
88
|
+
discountId?: string | null;
|
|
89
|
+
code: string;
|
|
90
|
+
type: DiscountType;
|
|
91
|
+
amount: number;
|
|
92
|
+
currency: string;
|
|
93
|
+
freeShipping: boolean;
|
|
94
|
+
validUntil: string | null;
|
|
95
|
+
snapshotHash: string;
|
|
96
|
+
} | null;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
interface DiscountsClientOptions {
|
|
100
|
+
apiKey: string;
|
|
101
|
+
baseUrl?: string;
|
|
102
|
+
fetch?: typeof globalThis.fetch;
|
|
103
|
+
timeoutMs?: number;
|
|
104
|
+
}
|
|
105
|
+
interface PaginatedDiscounts {
|
|
106
|
+
data: Discount[];
|
|
107
|
+
pagination?: {
|
|
108
|
+
cursor: string | null;
|
|
109
|
+
hasMore: boolean;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
declare class DiscountsClient {
|
|
114
|
+
private readonly apiKey;
|
|
115
|
+
private readonly baseUrl;
|
|
116
|
+
private readonly fetchImpl;
|
|
117
|
+
private readonly timeoutMs;
|
|
118
|
+
constructor(opts: DiscountsClientOptions);
|
|
119
|
+
request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
|
120
|
+
private requestEnvelope;
|
|
121
|
+
create(input: CreateDiscountInput): Promise<Discount>;
|
|
122
|
+
list(params?: DiscountListParams): Promise<PaginatedDiscounts>;
|
|
123
|
+
get(id: string): Promise<Discount>;
|
|
124
|
+
update(id: string, input: UpdateDiscountInput): Promise<Discount>;
|
|
125
|
+
delete(id: string): Promise<void>;
|
|
126
|
+
validate(code: string, orderTotal: number, customerId?: string): Promise<DiscountValidationResult>;
|
|
127
|
+
preview(input: DiscountPreviewInput): Promise<DiscountPreviewResult>;
|
|
128
|
+
applyToCart(cartId: string, code: string): Promise<Cart>;
|
|
129
|
+
removeFromCart(cartId: string): Promise<Cart>;
|
|
130
|
+
}
|
|
131
|
+
declare function createDiscountsClient(opts: DiscountsClientOptions): DiscountsClient;
|
|
132
|
+
|
|
133
|
+
declare class ThrottleDiscountsError extends Error {
|
|
134
|
+
readonly code: string;
|
|
135
|
+
readonly statusCode: number;
|
|
136
|
+
readonly details?: unknown;
|
|
137
|
+
constructor(args: {
|
|
138
|
+
code: string;
|
|
139
|
+
message: string;
|
|
140
|
+
statusCode: number;
|
|
141
|
+
details?: unknown;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { type Cart, type CreateDiscountInput, type Discount, type DiscountListParams, type DiscountPreviewInput, type DiscountPreviewResult, type DiscountType, type DiscountValidationResult, DiscountsClient, type DiscountsClientOptions, type PaginatedDiscounts, ThrottleDiscountsError, type UpdateDiscountInput, createDiscountsClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
type DiscountType = 'percentage' | 'fixed_amount';
|
|
2
|
+
interface Discount {
|
|
3
|
+
id: string;
|
|
4
|
+
code: string | null;
|
|
5
|
+
name: string;
|
|
6
|
+
type: DiscountType;
|
|
7
|
+
value: number;
|
|
8
|
+
conditions: Record<string, unknown>;
|
|
9
|
+
usageLimit?: number | null;
|
|
10
|
+
usageCount: number;
|
|
11
|
+
maxRedemptionsPerCustomer?: number | null;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
startsAt?: string | null;
|
|
14
|
+
endsAt?: string | null;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
}
|
|
18
|
+
interface CreateDiscountInput {
|
|
19
|
+
storeId?: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
name: string;
|
|
22
|
+
type: DiscountType;
|
|
23
|
+
value: number;
|
|
24
|
+
conditions?: Record<string, unknown>;
|
|
25
|
+
usageLimit?: number;
|
|
26
|
+
maxRedemptionsPerCustomer?: number | null;
|
|
27
|
+
startsAt?: string | Date;
|
|
28
|
+
endsAt?: string | Date;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface UpdateDiscountInput {
|
|
32
|
+
name?: string;
|
|
33
|
+
value?: number;
|
|
34
|
+
conditions?: Record<string, unknown>;
|
|
35
|
+
usageLimit?: number;
|
|
36
|
+
maxRedemptionsPerCustomer?: number | null;
|
|
37
|
+
isActive?: boolean;
|
|
38
|
+
startsAt?: string | Date;
|
|
39
|
+
endsAt?: string | Date;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
interface DiscountListParams {
|
|
43
|
+
storeId?: string;
|
|
44
|
+
isActive?: boolean;
|
|
45
|
+
code?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
sort?: 'asc' | 'desc';
|
|
49
|
+
}
|
|
50
|
+
interface DiscountValidationResult {
|
|
51
|
+
valid: boolean;
|
|
52
|
+
discount?: Discount;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
interface DiscountPreviewInput {
|
|
56
|
+
code: string;
|
|
57
|
+
currency: string;
|
|
58
|
+
subtotal: number;
|
|
59
|
+
customerId?: string;
|
|
60
|
+
lineItems?: Array<{
|
|
61
|
+
id?: string;
|
|
62
|
+
quantity: number;
|
|
63
|
+
unitPrice: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
interface DiscountPreviewResult {
|
|
67
|
+
valid: boolean;
|
|
68
|
+
code: string;
|
|
69
|
+
type?: DiscountType;
|
|
70
|
+
value?: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
subtotal: number;
|
|
73
|
+
discountTotal: number;
|
|
74
|
+
total: number;
|
|
75
|
+
discount?: Discount;
|
|
76
|
+
message: string | null;
|
|
77
|
+
}
|
|
78
|
+
interface Cart {
|
|
79
|
+
id: string;
|
|
80
|
+
currency: string;
|
|
81
|
+
subtotal: number;
|
|
82
|
+
discountTotal: number;
|
|
83
|
+
shippingTotal: number;
|
|
84
|
+
taxTotal: number;
|
|
85
|
+
total: number;
|
|
86
|
+
discountCode?: string | null;
|
|
87
|
+
appliedDiscount?: {
|
|
88
|
+
discountId?: string | null;
|
|
89
|
+
code: string;
|
|
90
|
+
type: DiscountType;
|
|
91
|
+
amount: number;
|
|
92
|
+
currency: string;
|
|
93
|
+
freeShipping: boolean;
|
|
94
|
+
validUntil: string | null;
|
|
95
|
+
snapshotHash: string;
|
|
96
|
+
} | null;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
interface DiscountsClientOptions {
|
|
100
|
+
apiKey: string;
|
|
101
|
+
baseUrl?: string;
|
|
102
|
+
fetch?: typeof globalThis.fetch;
|
|
103
|
+
timeoutMs?: number;
|
|
104
|
+
}
|
|
105
|
+
interface PaginatedDiscounts {
|
|
106
|
+
data: Discount[];
|
|
107
|
+
pagination?: {
|
|
108
|
+
cursor: string | null;
|
|
109
|
+
hasMore: boolean;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
declare class DiscountsClient {
|
|
114
|
+
private readonly apiKey;
|
|
115
|
+
private readonly baseUrl;
|
|
116
|
+
private readonly fetchImpl;
|
|
117
|
+
private readonly timeoutMs;
|
|
118
|
+
constructor(opts: DiscountsClientOptions);
|
|
119
|
+
request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
|
120
|
+
private requestEnvelope;
|
|
121
|
+
create(input: CreateDiscountInput): Promise<Discount>;
|
|
122
|
+
list(params?: DiscountListParams): Promise<PaginatedDiscounts>;
|
|
123
|
+
get(id: string): Promise<Discount>;
|
|
124
|
+
update(id: string, input: UpdateDiscountInput): Promise<Discount>;
|
|
125
|
+
delete(id: string): Promise<void>;
|
|
126
|
+
validate(code: string, orderTotal: number, customerId?: string): Promise<DiscountValidationResult>;
|
|
127
|
+
preview(input: DiscountPreviewInput): Promise<DiscountPreviewResult>;
|
|
128
|
+
applyToCart(cartId: string, code: string): Promise<Cart>;
|
|
129
|
+
removeFromCart(cartId: string): Promise<Cart>;
|
|
130
|
+
}
|
|
131
|
+
declare function createDiscountsClient(opts: DiscountsClientOptions): DiscountsClient;
|
|
132
|
+
|
|
133
|
+
declare class ThrottleDiscountsError extends Error {
|
|
134
|
+
readonly code: string;
|
|
135
|
+
readonly statusCode: number;
|
|
136
|
+
readonly details?: unknown;
|
|
137
|
+
constructor(args: {
|
|
138
|
+
code: string;
|
|
139
|
+
message: string;
|
|
140
|
+
statusCode: number;
|
|
141
|
+
details?: unknown;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { type Cart, type CreateDiscountInput, type Discount, type DiscountListParams, type DiscountPreviewInput, type DiscountPreviewResult, type DiscountType, type DiscountValidationResult, DiscountsClient, type DiscountsClientOptions, type PaginatedDiscounts, ThrottleDiscountsError, type UpdateDiscountInput, createDiscountsClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ThrottleDiscountsError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
statusCode;
|
|
5
|
+
details;
|
|
6
|
+
constructor(args) {
|
|
7
|
+
super(args.message);
|
|
8
|
+
this.name = "ThrottleDiscountsError";
|
|
9
|
+
this.code = args.code;
|
|
10
|
+
this.statusCode = args.statusCode;
|
|
11
|
+
this.details = args.details;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/client.ts
|
|
16
|
+
var DiscountsClient = class {
|
|
17
|
+
apiKey;
|
|
18
|
+
baseUrl;
|
|
19
|
+
fetchImpl;
|
|
20
|
+
timeoutMs;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
if (!opts.apiKey) throw new Error("DiscountsClient: apiKey is required");
|
|
23
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
24
|
+
if (!fetchImpl) throw new Error("DiscountsClient: fetch is required");
|
|
25
|
+
this.apiKey = opts.apiKey;
|
|
26
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
27
|
+
this.fetchImpl = fetchImpl;
|
|
28
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
29
|
+
}
|
|
30
|
+
async request(method, path, body) {
|
|
31
|
+
const envelope = await this.requestEnvelope(method, path, body);
|
|
32
|
+
return envelope.data;
|
|
33
|
+
}
|
|
34
|
+
async requestEnvelope(method, path, body) {
|
|
35
|
+
const url = path.startsWith("http") ? path : `${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`;
|
|
36
|
+
const ctrl = new AbortController();
|
|
37
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
38
|
+
try {
|
|
39
|
+
const res = await this.fetchImpl(url, {
|
|
40
|
+
method,
|
|
41
|
+
headers: {
|
|
42
|
+
"x-api-key": this.apiKey,
|
|
43
|
+
"content-type": "application/json"
|
|
44
|
+
},
|
|
45
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
46
|
+
signal: ctrl.signal
|
|
47
|
+
});
|
|
48
|
+
const json = await res.json().catch(() => ({}));
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new ThrottleDiscountsError({
|
|
51
|
+
code: json?.error?.code ?? "unknown_error",
|
|
52
|
+
message: json?.error?.message ?? `HTTP ${res.status}`,
|
|
53
|
+
statusCode: res.status,
|
|
54
|
+
details: json?.error?.details
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
data: json.data,
|
|
59
|
+
meta: json.meta
|
|
60
|
+
};
|
|
61
|
+
} finally {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
create(input) {
|
|
66
|
+
return this.request("POST", "/api/v1/discounts", serializeDates(input));
|
|
67
|
+
}
|
|
68
|
+
async list(params = {}) {
|
|
69
|
+
const query = new URLSearchParams();
|
|
70
|
+
if (params.storeId) query.set("storeId", params.storeId);
|
|
71
|
+
if (params.isActive !== void 0) query.set("isActive", String(params.isActive));
|
|
72
|
+
if (params.code) query.set("code", params.code);
|
|
73
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
74
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
75
|
+
if (params.sort) query.set("sort", params.sort);
|
|
76
|
+
const suffix = query.toString() ? `?${query}` : "";
|
|
77
|
+
const envelope = await this.requestEnvelope("GET", `/api/v1/discounts${suffix}`);
|
|
78
|
+
return {
|
|
79
|
+
data: envelope.data,
|
|
80
|
+
pagination: envelope.meta?.pagination ?? void 0
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
get(id) {
|
|
84
|
+
return this.request("GET", `/api/v1/discounts/${encodeURIComponent(id)}`);
|
|
85
|
+
}
|
|
86
|
+
update(id, input) {
|
|
87
|
+
return this.request(
|
|
88
|
+
"PATCH",
|
|
89
|
+
`/api/v1/discounts/${encodeURIComponent(id)}`,
|
|
90
|
+
serializeDates(input)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
delete(id) {
|
|
94
|
+
return this.request("DELETE", `/api/v1/discounts/${encodeURIComponent(id)}`);
|
|
95
|
+
}
|
|
96
|
+
validate(code, orderTotal, customerId) {
|
|
97
|
+
return this.request("POST", "/api/v1/discounts/validate", {
|
|
98
|
+
code,
|
|
99
|
+
orderTotal,
|
|
100
|
+
...customerId ? { customerId } : {}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
preview(input) {
|
|
104
|
+
return this.request("POST", "/api/v1/discounts/preview", input);
|
|
105
|
+
}
|
|
106
|
+
applyToCart(cartId, code) {
|
|
107
|
+
return this.request(
|
|
108
|
+
"POST",
|
|
109
|
+
`/api/v1/carts/${encodeURIComponent(cartId)}/apply-discount`,
|
|
110
|
+
{
|
|
111
|
+
code
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
removeFromCart(cartId) {
|
|
116
|
+
return this.request("DELETE", `/api/v1/carts/${encodeURIComponent(cartId)}/discount`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function createDiscountsClient(opts) {
|
|
120
|
+
return new DiscountsClient(opts);
|
|
121
|
+
}
|
|
122
|
+
function serializeDates(input) {
|
|
123
|
+
const out = { ...input };
|
|
124
|
+
for (const key of ["startsAt", "endsAt"]) {
|
|
125
|
+
const value = out[key];
|
|
126
|
+
if (value instanceof Date) out[key] = value.toISOString();
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
DiscountsClient,
|
|
132
|
+
ThrottleDiscountsError,
|
|
133
|
+
createDiscountsClient
|
|
134
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usethrottle/discounts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless TypeScript client for Throttle discount and promotion code APIs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md", "CLAUDE.md"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.7.0",
|
|
25
|
+
"vitest": "^2.1.9"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
}
|
|
33
|
+
}
|