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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +490 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +201 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +383 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +34 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +147 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +133 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +80 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +4 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/lib.d.ts +137 -0
  32. package/dist/component/lib.d.ts.map +1 -0
  33. package/dist/component/lib.js +241 -0
  34. package/dist/component/lib.js.map +1 -0
  35. package/dist/component/schema.d.ts +77 -0
  36. package/dist/component/schema.d.ts.map +1 -0
  37. package/dist/component/schema.js +45 -0
  38. package/dist/component/schema.js.map +1 -0
  39. package/package.json +106 -0
  40. package/src/client/_generated/_ignore.ts +1 -0
  41. package/src/client/index.ts +570 -0
  42. package/src/client/setup.test.ts +26 -0
  43. package/src/component/_generated/api.ts +50 -0
  44. package/src/component/_generated/component.ts +217 -0
  45. package/src/component/_generated/dataModel.ts +60 -0
  46. package/src/component/_generated/server.ts +169 -0
  47. package/src/component/convex.config.ts +5 -0
  48. package/src/component/lib.test.ts +110 -0
  49. package/src/component/lib.ts +278 -0
  50. package/src/component/schema.ts +58 -0
  51. package/src/component/setup.test.ts +11 -0
  52. 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 };