convex-paystack 0.0.1
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/LICENSE +201 -0
- package/README.md +490 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +201 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +383 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +34 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +147 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +133 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +80 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +4 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +137 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +241 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +77 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +45 -0
- package/dist/component/schema.js.map +1 -0
- package/package.json +106 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +570 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +50 -0
- package/src/component/_generated/component.ts +217 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +169 -0
- package/src/component/convex.config.ts +5 -0
- package/src/component/lib.test.ts +110 -0
- package/src/component/lib.ts +278 -0
- package/src/component/schema.ts +58 -0
- package/src/component/setup.test.ts +11 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server.js";
|
|
3
|
+
|
|
4
|
+
const transactionStatusValidator = v.union(
|
|
5
|
+
v.literal("pending"),
|
|
6
|
+
v.literal("success"),
|
|
7
|
+
v.literal("failed"),
|
|
8
|
+
v.literal("abandoned"),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const subscriptionStatusValidator = v.union(
|
|
12
|
+
v.literal("active"),
|
|
13
|
+
v.literal("non-renewing"),
|
|
14
|
+
v.literal("attention"),
|
|
15
|
+
v.literal("completed"),
|
|
16
|
+
v.literal("cancelled"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const transactionValidator = v.object({
|
|
20
|
+
_id: v.id("transactions"),
|
|
21
|
+
_creationTime: v.number(),
|
|
22
|
+
reference: v.string(),
|
|
23
|
+
customerEmail: v.string(),
|
|
24
|
+
amount: v.number(),
|
|
25
|
+
currency: v.string(),
|
|
26
|
+
status: transactionStatusValidator,
|
|
27
|
+
channel: v.optional(v.string()),
|
|
28
|
+
gatewayResponse: v.optional(v.string()),
|
|
29
|
+
authorizationCode: v.optional(v.string()),
|
|
30
|
+
paidAt: v.optional(v.number()),
|
|
31
|
+
metadata: v.optional(v.string()),
|
|
32
|
+
createdAt: v.number(),
|
|
33
|
+
updatedAt: v.number(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const subscriptionValidator = v.object({
|
|
37
|
+
_id: v.id("subscriptions"),
|
|
38
|
+
_creationTime: v.number(),
|
|
39
|
+
subscriptionCode: v.string(),
|
|
40
|
+
emailToken: v.optional(v.string()),
|
|
41
|
+
customerEmail: v.string(),
|
|
42
|
+
customerCode: v.optional(v.string()),
|
|
43
|
+
planCode: v.string(),
|
|
44
|
+
status: subscriptionStatusValidator,
|
|
45
|
+
amount: v.optional(v.number()),
|
|
46
|
+
nextPaymentDate: v.optional(v.number()),
|
|
47
|
+
createdAt: v.number(),
|
|
48
|
+
updatedAt: v.number(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const webhookEventValidator = v.object({
|
|
52
|
+
_id: v.id("webhookEvents"),
|
|
53
|
+
_creationTime: v.number(),
|
|
54
|
+
eventId: v.string(),
|
|
55
|
+
eventType: v.string(),
|
|
56
|
+
reference: v.optional(v.string()),
|
|
57
|
+
payload: v.string(),
|
|
58
|
+
receivedAt: v.number(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─── Queries ────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export const getTransaction = query({
|
|
64
|
+
args: { reference: v.string() },
|
|
65
|
+
returns: v.union(v.null(), transactionValidator),
|
|
66
|
+
handler: async (ctx, args) => {
|
|
67
|
+
return await ctx.db
|
|
68
|
+
.query("transactions")
|
|
69
|
+
.withIndex("by_reference", (q) => q.eq("reference", args.reference))
|
|
70
|
+
.first();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const listTransactions = query({
|
|
75
|
+
args: { customerEmail: v.string(), limit: v.optional(v.number()) },
|
|
76
|
+
returns: v.array(transactionValidator),
|
|
77
|
+
handler: async (ctx, args) => {
|
|
78
|
+
return await ctx.db
|
|
79
|
+
.query("transactions")
|
|
80
|
+
.withIndex("by_customerEmail", (q) => q.eq("customerEmail", args.customerEmail))
|
|
81
|
+
.order("desc")
|
|
82
|
+
.take(args.limit ?? 50);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const getSubscription = query({
|
|
87
|
+
args: { subscriptionCode: v.string() },
|
|
88
|
+
returns: v.union(v.null(), subscriptionValidator),
|
|
89
|
+
handler: async (ctx, args) => {
|
|
90
|
+
return await ctx.db
|
|
91
|
+
.query("subscriptions")
|
|
92
|
+
.withIndex("by_subscriptionCode", (q) =>
|
|
93
|
+
q.eq("subscriptionCode", args.subscriptionCode),
|
|
94
|
+
)
|
|
95
|
+
.first();
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const listSubscriptions = query({
|
|
100
|
+
args: { customerEmail: v.string() },
|
|
101
|
+
returns: v.array(subscriptionValidator),
|
|
102
|
+
handler: async (ctx, args) => {
|
|
103
|
+
return await ctx.db
|
|
104
|
+
.query("subscriptions")
|
|
105
|
+
.withIndex("by_customerEmail", (q) => q.eq("customerEmail", args.customerEmail))
|
|
106
|
+
.order("desc")
|
|
107
|
+
.collect();
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const hasActiveSubscription = query({
|
|
112
|
+
args: { customerEmail: v.string() },
|
|
113
|
+
returns: v.boolean(),
|
|
114
|
+
handler: async (ctx, args) => {
|
|
115
|
+
const sub = await ctx.db
|
|
116
|
+
.query("subscriptions")
|
|
117
|
+
.withIndex("by_customerEmail", (q) => q.eq("customerEmail", args.customerEmail))
|
|
118
|
+
.order("desc")
|
|
119
|
+
.first();
|
|
120
|
+
return sub?.status === "active" || sub?.status === "non-renewing";
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reads the raw webhook event log, newest first — useful for an audit
|
|
126
|
+
* trail or a live "what just happened" console in your own app. Every
|
|
127
|
+
* event Paystack has ever sent to this component's webhook handler is
|
|
128
|
+
* recorded here for idempotency, whether or not it triggered a state
|
|
129
|
+
* change.
|
|
130
|
+
*/
|
|
131
|
+
export const listRecentEvents = query({
|
|
132
|
+
args: { limit: v.optional(v.number()) },
|
|
133
|
+
returns: v.array(webhookEventValidator),
|
|
134
|
+
handler: async (ctx, args) => {
|
|
135
|
+
return await ctx.db
|
|
136
|
+
.query("webhookEvents")
|
|
137
|
+
.order("desc")
|
|
138
|
+
.take(args.limit ?? 50);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Aggregate row counts across all three tables — enough for a small
|
|
144
|
+
* dashboard stat row. This does a full table scan, so it's fine for the
|
|
145
|
+
* data volumes a demo or small integration produces; a high-volume
|
|
146
|
+
* production app should track its own counters instead of calling this
|
|
147
|
+
* on every render.
|
|
148
|
+
*/
|
|
149
|
+
export const getStats = query({
|
|
150
|
+
args: {},
|
|
151
|
+
returns: v.object({
|
|
152
|
+
transactions: v.number(),
|
|
153
|
+
subscriptions: v.number(),
|
|
154
|
+
events: v.number(),
|
|
155
|
+
}),
|
|
156
|
+
handler: async (ctx) => {
|
|
157
|
+
const [transactions, subscriptions, events] = await Promise.all([
|
|
158
|
+
ctx.db.query("transactions").collect(),
|
|
159
|
+
ctx.db.query("subscriptions").collect(),
|
|
160
|
+
ctx.db.query("webhookEvents").collect(),
|
|
161
|
+
]);
|
|
162
|
+
return {
|
|
163
|
+
transactions: transactions.length,
|
|
164
|
+
subscriptions: subscriptions.length,
|
|
165
|
+
events: events.length,
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── Mutations ──────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
export const recordTransaction = mutation({
|
|
173
|
+
args: {
|
|
174
|
+
reference: v.string(),
|
|
175
|
+
customerEmail: v.string(),
|
|
176
|
+
amount: v.number(),
|
|
177
|
+
currency: v.string(),
|
|
178
|
+
status: transactionStatusValidator,
|
|
179
|
+
channel: v.optional(v.string()),
|
|
180
|
+
gatewayResponse: v.optional(v.string()),
|
|
181
|
+
authorizationCode: v.optional(v.string()),
|
|
182
|
+
paidAt: v.optional(v.number()),
|
|
183
|
+
metadata: v.optional(v.string()),
|
|
184
|
+
},
|
|
185
|
+
returns: v.id("transactions"),
|
|
186
|
+
handler: async (ctx, args) => {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const existing = await ctx.db
|
|
189
|
+
.query("transactions")
|
|
190
|
+
.withIndex("by_reference", (q) => q.eq("reference", args.reference))
|
|
191
|
+
.first();
|
|
192
|
+
|
|
193
|
+
if (existing) {
|
|
194
|
+
await ctx.db.patch(existing._id, { ...args, updatedAt: now });
|
|
195
|
+
return existing._id;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return await ctx.db.insert("transactions", {
|
|
199
|
+
...args,
|
|
200
|
+
createdAt: now,
|
|
201
|
+
updatedAt: now,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export const recordSubscriptionEvent = mutation({
|
|
207
|
+
args: {
|
|
208
|
+
subscriptionCode: v.string(),
|
|
209
|
+
emailToken: v.optional(v.string()),
|
|
210
|
+
customerEmail: v.string(),
|
|
211
|
+
customerCode: v.optional(v.string()),
|
|
212
|
+
planCode: v.string(),
|
|
213
|
+
status: subscriptionStatusValidator,
|
|
214
|
+
amount: v.optional(v.number()),
|
|
215
|
+
nextPaymentDate: v.optional(v.number()),
|
|
216
|
+
},
|
|
217
|
+
returns: v.id("subscriptions"),
|
|
218
|
+
handler: async (ctx, args) => {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const existing = await ctx.db
|
|
221
|
+
.query("subscriptions")
|
|
222
|
+
.withIndex("by_subscriptionCode", (q) =>
|
|
223
|
+
q.eq("subscriptionCode", args.subscriptionCode),
|
|
224
|
+
)
|
|
225
|
+
.first();
|
|
226
|
+
|
|
227
|
+
if (existing) {
|
|
228
|
+
await ctx.db.patch(existing._id, { ...args, updatedAt: now });
|
|
229
|
+
return existing._id;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return await ctx.db.insert("subscriptions", {
|
|
233
|
+
...args,
|
|
234
|
+
createdAt: now,
|
|
235
|
+
updatedAt: now,
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
export const updateSubscriptionStatus = mutation({
|
|
241
|
+
args: {
|
|
242
|
+
subscriptionCode: v.string(),
|
|
243
|
+
status: subscriptionStatusValidator,
|
|
244
|
+
},
|
|
245
|
+
returns: v.null(),
|
|
246
|
+
handler: async (ctx, args) => {
|
|
247
|
+
const existing = await ctx.db
|
|
248
|
+
.query("subscriptions")
|
|
249
|
+
.withIndex("by_subscriptionCode", (q) =>
|
|
250
|
+
q.eq("subscriptionCode", args.subscriptionCode),
|
|
251
|
+
)
|
|
252
|
+
.first();
|
|
253
|
+
if (!existing) return null;
|
|
254
|
+
await ctx.db.patch(existing._id, { status: args.status, updatedAt: Date.now() });
|
|
255
|
+
return null;
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export const checkAndRecordEvent = mutation({
|
|
260
|
+
args: {
|
|
261
|
+
eventId: v.string(),
|
|
262
|
+
eventType: v.string(),
|
|
263
|
+
reference: v.optional(v.string()),
|
|
264
|
+
payload: v.string(),
|
|
265
|
+
},
|
|
266
|
+
returns: v.object({ alreadyProcessed: v.boolean() }),
|
|
267
|
+
handler: async (ctx, args) => {
|
|
268
|
+
const existing = await ctx.db
|
|
269
|
+
.query("webhookEvents")
|
|
270
|
+
.withIndex("by_eventId", (q) => q.eq("eventId", args.eventId))
|
|
271
|
+
.first();
|
|
272
|
+
if (existing) {
|
|
273
|
+
return { alreadyProcessed: true };
|
|
274
|
+
}
|
|
275
|
+
await ctx.db.insert("webhookEvents", { ...args, receivedAt: Date.now() });
|
|
276
|
+
return { alreadyProcessed: false };
|
|
277
|
+
},
|
|
278
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
export default defineSchema({
|
|
5
|
+
transactions: defineTable({
|
|
6
|
+
reference: v.string(),
|
|
7
|
+
customerEmail: v.string(),
|
|
8
|
+
amount: v.number(),
|
|
9
|
+
currency: v.string(),
|
|
10
|
+
status: v.union(
|
|
11
|
+
v.literal("pending"),
|
|
12
|
+
v.literal("success"),
|
|
13
|
+
v.literal("failed"),
|
|
14
|
+
v.literal("abandoned"),
|
|
15
|
+
),
|
|
16
|
+
channel: v.optional(v.string()),
|
|
17
|
+
gatewayResponse: v.optional(v.string()),
|
|
18
|
+
authorizationCode: v.optional(v.string()),
|
|
19
|
+
paidAt: v.optional(v.number()),
|
|
20
|
+
metadata: v.optional(v.string()),
|
|
21
|
+
createdAt: v.number(),
|
|
22
|
+
updatedAt: v.number(),
|
|
23
|
+
})
|
|
24
|
+
.index("by_reference", ["reference"])
|
|
25
|
+
.index("by_customerEmail", ["customerEmail"]),
|
|
26
|
+
|
|
27
|
+
subscriptions: defineTable({
|
|
28
|
+
subscriptionCode: v.string(),
|
|
29
|
+
emailToken: v.optional(v.string()),
|
|
30
|
+
customerEmail: v.string(),
|
|
31
|
+
customerCode: v.optional(v.string()),
|
|
32
|
+
planCode: v.string(),
|
|
33
|
+
status: v.union(
|
|
34
|
+
v.literal("active"),
|
|
35
|
+
v.literal("non-renewing"),
|
|
36
|
+
v.literal("attention"),
|
|
37
|
+
v.literal("completed"),
|
|
38
|
+
v.literal("cancelled"),
|
|
39
|
+
),
|
|
40
|
+
amount: v.optional(v.number()),
|
|
41
|
+
nextPaymentDate: v.optional(v.number()),
|
|
42
|
+
createdAt: v.number(),
|
|
43
|
+
updatedAt: v.number(),
|
|
44
|
+
})
|
|
45
|
+
.index("by_subscriptionCode", ["subscriptionCode"])
|
|
46
|
+
.index("by_customerEmail", ["customerEmail"])
|
|
47
|
+
.index("by_status", ["status"]),
|
|
48
|
+
|
|
49
|
+
webhookEvents: defineTable({
|
|
50
|
+
eventId: v.string(),
|
|
51
|
+
eventType: v.string(),
|
|
52
|
+
reference: v.optional(v.string()),
|
|
53
|
+
payload: v.string(),
|
|
54
|
+
receivedAt: v.number(),
|
|
55
|
+
})
|
|
56
|
+
.index("by_eventId", ["eventId"])
|
|
57
|
+
.index("by_eventType", ["eventType"]),
|
|
58
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import schema from "./schema.js";
|
|
4
|
+
import { convexTest } from "convex-test";
|
|
5
|
+
export const modules = import.meta.glob("./**/*.*s");
|
|
6
|
+
|
|
7
|
+
export function initConvexTest() {
|
|
8
|
+
const t = convexTest(schema, modules);
|
|
9
|
+
return t;
|
|
10
|
+
}
|
|
11
|
+
test("setup", () => {});
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import type { TestConvex } from "convex-test";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import schema from "./component/schema.js";
|
|
5
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register the component with the test convex instance.
|
|
9
|
+
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
10
|
+
* @param name - The name of the component, as registered in convex.config.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function register(
|
|
13
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
14
|
+
name: string = "convexPaystack",
|
|
15
|
+
) {
|
|
16
|
+
t.registerComponent(name, schema, modules);
|
|
17
|
+
}
|
|
18
|
+
export default { register, schema, modules };
|