@pulgueta/epayco-convex 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.
Files changed (169) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +945 -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 +222 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +355 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +78 -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 +580 -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 +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/banks.d.ts +14 -0
  28. package/dist/component/banks.d.ts.map +1 -0
  29. package/dist/component/banks.js +37 -0
  30. package/dist/component/banks.js.map +1 -0
  31. package/dist/component/cashApi.d.ts +59 -0
  32. package/dist/component/cashApi.d.ts.map +1 -0
  33. package/dist/component/cashApi.js +88 -0
  34. package/dist/component/cashApi.js.map +1 -0
  35. package/dist/component/chargesApi.d.ts +64 -0
  36. package/dist/component/chargesApi.d.ts.map +1 -0
  37. package/dist/component/chargesApi.js +106 -0
  38. package/dist/component/chargesApi.js.map +1 -0
  39. package/dist/component/convex.config.d.ts +3 -0
  40. package/dist/component/convex.config.d.ts.map +1 -0
  41. package/dist/component/convex.config.js +6 -0
  42. package/dist/component/convex.config.js.map +1 -0
  43. package/dist/component/customers.d.ts +67 -0
  44. package/dist/component/customers.d.ts.map +1 -0
  45. package/dist/component/customers.js +103 -0
  46. package/dist/component/customers.js.map +1 -0
  47. package/dist/component/customersApi.d.ts +99 -0
  48. package/dist/component/customersApi.d.ts.map +1 -0
  49. package/dist/component/customersApi.js +176 -0
  50. package/dist/component/customersApi.js.map +1 -0
  51. package/dist/component/daviplataApi.d.ts +43 -0
  52. package/dist/component/daviplataApi.d.ts.map +1 -0
  53. package/dist/component/daviplataApi.js +103 -0
  54. package/dist/component/daviplataApi.js.map +1 -0
  55. package/dist/component/epaycoClient.d.ts +84 -0
  56. package/dist/component/epaycoClient.d.ts.map +1 -0
  57. package/dist/component/epaycoClient.js +422 -0
  58. package/dist/component/epaycoClient.js.map +1 -0
  59. package/dist/component/payloads.d.ts +34 -0
  60. package/dist/component/payloads.d.ts.map +1 -0
  61. package/dist/component/payloads.js +45 -0
  62. package/dist/component/payloads.js.map +1 -0
  63. package/dist/component/plans.d.ts +47 -0
  64. package/dist/component/plans.d.ts.map +1 -0
  65. package/dist/component/plans.js +83 -0
  66. package/dist/component/plans.js.map +1 -0
  67. package/dist/component/plansApi.d.ts +64 -0
  68. package/dist/component/plansApi.d.ts.map +1 -0
  69. package/dist/component/plansApi.js +121 -0
  70. package/dist/component/plansApi.js.map +1 -0
  71. package/dist/component/pseApi.d.ts +68 -0
  72. package/dist/component/pseApi.d.ts.map +1 -0
  73. package/dist/component/pseApi.js +113 -0
  74. package/dist/component/pseApi.js.map +1 -0
  75. package/dist/component/rateLimits.d.ts +69 -0
  76. package/dist/component/rateLimits.d.ts.map +1 -0
  77. package/dist/component/rateLimits.js +67 -0
  78. package/dist/component/rateLimits.js.map +1 -0
  79. package/dist/component/safetypayApi.d.ts +35 -0
  80. package/dist/component/safetypayApi.d.ts.map +1 -0
  81. package/dist/component/safetypayApi.js +68 -0
  82. package/dist/component/safetypayApi.js.map +1 -0
  83. package/dist/component/schema.d.ts +200 -0
  84. package/dist/component/schema.d.ts.map +1 -0
  85. package/dist/component/schema.js +104 -0
  86. package/dist/component/schema.js.map +1 -0
  87. package/dist/component/signature.d.ts +11 -0
  88. package/dist/component/signature.d.ts.map +1 -0
  89. package/dist/component/signature.js +28 -0
  90. package/dist/component/signature.js.map +1 -0
  91. package/dist/component/status.d.ts +12 -0
  92. package/dist/component/status.d.ts.map +1 -0
  93. package/dist/component/status.js +55 -0
  94. package/dist/component/status.js.map +1 -0
  95. package/dist/component/subscriptions.d.ts +69 -0
  96. package/dist/component/subscriptions.d.ts.map +1 -0
  97. package/dist/component/subscriptions.js +114 -0
  98. package/dist/component/subscriptions.js.map +1 -0
  99. package/dist/component/subscriptionsApi.d.ts +62 -0
  100. package/dist/component/subscriptionsApi.d.ts.map +1 -0
  101. package/dist/component/subscriptionsApi.js +147 -0
  102. package/dist/component/subscriptionsApi.js.map +1 -0
  103. package/dist/component/tokens.d.ts +31 -0
  104. package/dist/component/tokens.d.ts.map +1 -0
  105. package/dist/component/tokens.js +79 -0
  106. package/dist/component/tokens.js.map +1 -0
  107. package/dist/component/tokensApi.d.ts +18 -0
  108. package/dist/component/tokensApi.d.ts.map +1 -0
  109. package/dist/component/tokensApi.js +53 -0
  110. package/dist/component/tokensApi.js.map +1 -0
  111. package/dist/component/transactions.d.ts +103 -0
  112. package/dist/component/transactions.d.ts.map +1 -0
  113. package/dist/component/transactions.js +177 -0
  114. package/dist/component/transactions.js.map +1 -0
  115. package/dist/component/validators.d.ts +571 -0
  116. package/dist/component/validators.d.ts.map +1 -0
  117. package/dist/component/validators.js +203 -0
  118. package/dist/component/validators.js.map +1 -0
  119. package/dist/component/webhooks.d.ts +55 -0
  120. package/dist/component/webhooks.d.ts.map +1 -0
  121. package/dist/component/webhooks.js +172 -0
  122. package/dist/component/webhooks.js.map +1 -0
  123. package/dist/react/index.d.ts +16 -0
  124. package/dist/react/index.d.ts.map +1 -0
  125. package/dist/react/index.js +43 -0
  126. package/dist/react/index.js.map +1 -0
  127. package/package.json +106 -0
  128. package/src/client/_generated/_ignore.ts +1 -0
  129. package/src/client/index.test.ts +66 -0
  130. package/src/client/index.ts +633 -0
  131. package/src/client/setup.test.ts +26 -0
  132. package/src/component/_generated/api.ts +94 -0
  133. package/src/component/_generated/component.ts +809 -0
  134. package/src/component/_generated/dataModel.ts +60 -0
  135. package/src/component/_generated/server.ts +156 -0
  136. package/src/component/banks.ts +41 -0
  137. package/src/component/cashApi.ts +100 -0
  138. package/src/component/chargesApi.ts +119 -0
  139. package/src/component/convex.config.ts +7 -0
  140. package/src/component/customers.test.ts +122 -0
  141. package/src/component/customers.ts +116 -0
  142. package/src/component/customersApi.ts +206 -0
  143. package/src/component/daviplataApi.ts +119 -0
  144. package/src/component/epaycoApi.test.ts +110 -0
  145. package/src/component/epaycoClient.ts +578 -0
  146. package/src/component/payloads.ts +67 -0
  147. package/src/component/plans.test.ts +129 -0
  148. package/src/component/plans.ts +86 -0
  149. package/src/component/plansApi.ts +135 -0
  150. package/src/component/pseApi.ts +125 -0
  151. package/src/component/rateLimits.ts +67 -0
  152. package/src/component/safetypayApi.ts +78 -0
  153. package/src/component/schema.ts +124 -0
  154. package/src/component/setup.test.helper.ts +10 -0
  155. package/src/component/setup.test.ts +22 -0
  156. package/src/component/signature.ts +38 -0
  157. package/src/component/status.ts +71 -0
  158. package/src/component/subscriptions.test.ts +117 -0
  159. package/src/component/subscriptions.ts +128 -0
  160. package/src/component/subscriptionsApi.ts +172 -0
  161. package/src/component/tokens.ts +89 -0
  162. package/src/component/tokensApi.ts +63 -0
  163. package/src/component/transactions.test.ts +227 -0
  164. package/src/component/transactions.ts +200 -0
  165. package/src/component/validators.ts +245 -0
  166. package/src/component/webhooks.test.ts +137 -0
  167. package/src/component/webhooks.ts +229 -0
  168. package/src/react/index.ts +71 -0
  169. package/src/test.ts +13 -0
@@ -0,0 +1,129 @@
1
+ /// <reference types="vite/client" />
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
3
+ import { initConvexTest } from "./setup.test.helper.js";
4
+ import { api, internal } from "./_generated/api.js";
5
+
6
+ describe("plans", () => {
7
+ beforeEach(() => {
8
+ vi.useFakeTimers();
9
+ });
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+
14
+ test("upsertPlan creates a new plan", async () => {
15
+ const t = initConvexTest();
16
+ const id = await t.mutation(internal.plans.upsertPlan, {
17
+ epaycoPlanId: "plan_basic",
18
+ name: "Basic Plan",
19
+ description: "Basic monthly plan",
20
+ amount: 29900,
21
+ currency: "COP",
22
+ interval: "month",
23
+ intervalCount: 1,
24
+ trialDays: 7,
25
+ status: "active",
26
+ lastSyncedAt: 1000,
27
+ });
28
+ expect(id).toBeDefined();
29
+
30
+ const plan = await t.query(api.plans.getLocalPlan, {
31
+ epaycoPlanId: "plan_basic",
32
+ });
33
+ expect(plan).not.toBeNull();
34
+ expect(plan!.name).toBe("Basic Plan");
35
+ expect(plan!.amount).toBe(29900);
36
+ });
37
+
38
+ test("upsertPlan is idempotent with same timestamp", async () => {
39
+ const t = initConvexTest();
40
+ await t.mutation(internal.plans.upsertPlan, {
41
+ epaycoPlanId: "plan_pro",
42
+ name: "Pro Plan",
43
+ description: "Pro plan",
44
+ amount: 59900,
45
+ currency: "COP",
46
+ interval: "month",
47
+ intervalCount: 1,
48
+ trialDays: 0,
49
+ status: "active",
50
+ lastSyncedAt: 1000,
51
+ });
52
+
53
+ await t.mutation(internal.plans.upsertPlan, {
54
+ epaycoPlanId: "plan_pro",
55
+ name: "Pro Plan Updated",
56
+ lastSyncedAt: 1000,
57
+ });
58
+
59
+ const plan = await t.query(api.plans.getLocalPlan, {
60
+ epaycoPlanId: "plan_pro",
61
+ });
62
+ expect(plan!.name).toBe("Pro Plan");
63
+ });
64
+
65
+ test("listLocalPlans returns all plans", async () => {
66
+ const t = initConvexTest();
67
+ await t.mutation(internal.plans.upsertPlan, {
68
+ epaycoPlanId: "plan_1",
69
+ name: "Plan 1",
70
+ description: "First",
71
+ amount: 10000,
72
+ currency: "COP",
73
+ interval: "month",
74
+ intervalCount: 1,
75
+ trialDays: 0,
76
+ status: "active",
77
+ lastSyncedAt: 1000,
78
+ });
79
+ await t.mutation(internal.plans.upsertPlan, {
80
+ epaycoPlanId: "plan_2",
81
+ name: "Plan 2",
82
+ description: "Second",
83
+ amount: 20000,
84
+ currency: "COP",
85
+ interval: "year",
86
+ intervalCount: 1,
87
+ trialDays: 30,
88
+ status: "active",
89
+ lastSyncedAt: 1000,
90
+ });
91
+
92
+ const plans = await t.query(api.plans.listLocalPlans, {});
93
+ expect(plans).toHaveLength(2);
94
+ });
95
+
96
+ test("listLocalPlans filters by status", async () => {
97
+ const t = initConvexTest();
98
+ await t.mutation(internal.plans.upsertPlan, {
99
+ epaycoPlanId: "plan_active",
100
+ name: "Active",
101
+ description: "Active plan",
102
+ amount: 10000,
103
+ currency: "COP",
104
+ interval: "month",
105
+ intervalCount: 1,
106
+ trialDays: 0,
107
+ status: "active",
108
+ lastSyncedAt: 1000,
109
+ });
110
+ await t.mutation(internal.plans.upsertPlan, {
111
+ epaycoPlanId: "plan_deleted",
112
+ name: "Deleted",
113
+ description: "Deleted plan",
114
+ amount: 20000,
115
+ currency: "COP",
116
+ interval: "month",
117
+ intervalCount: 1,
118
+ trialDays: 0,
119
+ status: "deleted",
120
+ lastSyncedAt: 1000,
121
+ });
122
+
123
+ const activePlans = await t.query(api.plans.listLocalPlans, {
124
+ status: "active",
125
+ });
126
+ expect(activePlans).toHaveLength(1);
127
+ expect(activePlans[0].name).toBe("Active");
128
+ });
129
+ });
@@ -0,0 +1,86 @@
1
+ import { v } from "convex/values";
2
+ import { internalMutation, query } from "./_generated/server.js";
3
+
4
+ /** Local persistence for recurring plans. */
5
+
6
+ export const getLocalPlan = query({
7
+ args: { epaycoPlanId: v.string() },
8
+ returns: v.any(),
9
+ handler: async (ctx, args) => {
10
+ return await ctx.db
11
+ .query("plans")
12
+ .withIndex("by_epaycoPlanId", (q) =>
13
+ q.eq("epaycoPlanId", args.epaycoPlanId),
14
+ )
15
+ .first();
16
+ },
17
+ });
18
+
19
+ export const listLocalPlans = query({
20
+ args: { status: v.optional(v.string()), limit: v.optional(v.number()) },
21
+ returns: v.any(),
22
+ handler: async (ctx, args) => {
23
+ if (args.status) {
24
+ return await ctx.db
25
+ .query("plans")
26
+ .withIndex("by_status", (q) => q.eq("status", args.status!))
27
+ .take(args.limit ?? 100);
28
+ }
29
+ return await ctx.db.query("plans").take(args.limit ?? 100);
30
+ },
31
+ });
32
+
33
+ export const upsertPlan = internalMutation({
34
+ args: {
35
+ epaycoPlanId: v.string(),
36
+ name: v.optional(v.string()),
37
+ description: v.optional(v.string()),
38
+ amount: v.optional(v.number()),
39
+ currency: v.optional(v.string()),
40
+ interval: v.optional(v.string()),
41
+ intervalCount: v.optional(v.number()),
42
+ trialDays: v.optional(v.number()),
43
+ status: v.optional(v.string()),
44
+ lastSyncedAt: v.number(),
45
+ },
46
+ returns: v.id("plans"),
47
+ handler: async (ctx, args) => {
48
+ const existing = await ctx.db
49
+ .query("plans")
50
+ .withIndex("by_epaycoPlanId", (q) =>
51
+ q.eq("epaycoPlanId", args.epaycoPlanId),
52
+ )
53
+ .first();
54
+
55
+ if (existing) {
56
+ if (existing.lastSyncedAt >= args.lastSyncedAt) return existing._id;
57
+
58
+ const patch: Record<string, unknown> = { lastSyncedAt: args.lastSyncedAt };
59
+ if (args.name !== undefined) patch.name = args.name;
60
+ if (args.description !== undefined) patch.description = args.description;
61
+ if (args.amount !== undefined) patch.amount = args.amount;
62
+ if (args.currency !== undefined) patch.currency = args.currency;
63
+ if (args.interval !== undefined) patch.interval = args.interval;
64
+ if (args.intervalCount !== undefined)
65
+ patch.intervalCount = args.intervalCount;
66
+ if (args.trialDays !== undefined) patch.trialDays = args.trialDays;
67
+ if (args.status !== undefined) patch.status = args.status;
68
+
69
+ await ctx.db.patch(existing._id, patch);
70
+ return existing._id;
71
+ }
72
+
73
+ return await ctx.db.insert("plans", {
74
+ epaycoPlanId: args.epaycoPlanId,
75
+ name: args.name ?? "",
76
+ description: args.description ?? "",
77
+ amount: args.amount ?? 0,
78
+ currency: args.currency ?? "COP",
79
+ interval: args.interval ?? "month",
80
+ intervalCount: args.intervalCount ?? 1,
81
+ trialDays: args.trialDays ?? 0,
82
+ status: args.status ?? "active",
83
+ lastSyncedAt: args.lastSyncedAt,
84
+ });
85
+ },
86
+ });
@@ -0,0 +1,135 @@
1
+ import { v } from "convex/values";
2
+ import { action } from "./_generated/server.js";
3
+ import { internal } from "./_generated/api.js";
4
+ import { epaycoCredentialsValidator, planInfoValidator } from "./validators.js";
5
+ import { getEpaycoClient, unwrap } from "./epaycoClient.js";
6
+
7
+ /** Create a recurring plan. */
8
+ export const createPlan = action({
9
+ args: {
10
+ credentials: epaycoCredentialsValidator,
11
+ planInfo: planInfoValidator,
12
+ },
13
+ returns: v.any(),
14
+ handler: async (ctx, args) => {
15
+ const p = args.planInfo;
16
+ const epayco = getEpaycoClient(args.credentials);
17
+ const result = unwrap(
18
+ await epayco.plans.create({
19
+ id_plan: p.idPlan,
20
+ name: p.name,
21
+ description: p.description,
22
+ amount: p.amount,
23
+ currency: p.currency,
24
+ interval: p.interval,
25
+ interval_count: p.intervalCount,
26
+ trial_days: p.trialDays,
27
+ ...(p.iva !== undefined ? { iva: p.iva } : {}),
28
+ ...(p.ico !== undefined ? { ico: p.ico } : {}),
29
+ }),
30
+ );
31
+
32
+ await ctx.runMutation(internal.plans.upsertPlan, {
33
+ epaycoPlanId: p.idPlan,
34
+ name: p.name,
35
+ description: p.description,
36
+ amount: p.amount,
37
+ currency: p.currency,
38
+ interval: p.interval,
39
+ intervalCount: p.intervalCount,
40
+ trialDays: p.trialDays,
41
+ status: "active",
42
+ lastSyncedAt: Date.now(),
43
+ });
44
+
45
+ return result;
46
+ },
47
+ });
48
+
49
+ export const getPlan = action({
50
+ args: {
51
+ credentials: epaycoCredentialsValidator,
52
+ epaycoPlanId: v.string(),
53
+ },
54
+ returns: v.any(),
55
+ handler: async (_ctx, args) => {
56
+ const epayco = getEpaycoClient(args.credentials);
57
+ return unwrap(await epayco.plans.get(args.epaycoPlanId));
58
+ },
59
+ });
60
+
61
+ export const listPlans = action({
62
+ args: { credentials: epaycoCredentialsValidator },
63
+ returns: v.any(),
64
+ handler: async (_ctx, args) => {
65
+ const epayco = getEpaycoClient(args.credentials);
66
+ return unwrap(await epayco.plans.list());
67
+ },
68
+ });
69
+
70
+ export const updatePlan = action({
71
+ args: {
72
+ credentials: epaycoCredentialsValidator,
73
+ epaycoPlanId: v.string(),
74
+ name: v.optional(v.string()),
75
+ description: v.optional(v.string()),
76
+ amount: v.optional(v.number()),
77
+ currency: v.optional(v.string()),
78
+ interval: v.optional(v.string()),
79
+ intervalCount: v.optional(v.number()),
80
+ trialDays: v.optional(v.number()),
81
+ },
82
+ returns: v.any(),
83
+ handler: async (ctx, args) => {
84
+ const epayco = getEpaycoClient(args.credentials);
85
+ const result = unwrap(
86
+ await epayco.plans.update(args.epaycoPlanId, {
87
+ ...(args.name ? { name: args.name } : {}),
88
+ ...(args.description ? { description: args.description } : {}),
89
+ ...(args.amount !== undefined ? { amount: args.amount } : {}),
90
+ ...(args.currency ? { currency: args.currency } : {}),
91
+ ...(args.interval ? { interval: args.interval } : {}),
92
+ ...(args.intervalCount !== undefined
93
+ ? { interval_count: args.intervalCount }
94
+ : {}),
95
+ ...(args.trialDays !== undefined ? { trial_days: args.trialDays } : {}),
96
+ }),
97
+ );
98
+
99
+ await ctx.runMutation(internal.plans.upsertPlan, {
100
+ epaycoPlanId: args.epaycoPlanId,
101
+ ...(args.name !== undefined ? { name: args.name } : {}),
102
+ ...(args.description !== undefined ? { description: args.description } : {}),
103
+ ...(args.amount !== undefined ? { amount: args.amount } : {}),
104
+ ...(args.currency !== undefined ? { currency: args.currency } : {}),
105
+ ...(args.interval !== undefined ? { interval: args.interval } : {}),
106
+ ...(args.intervalCount !== undefined
107
+ ? { intervalCount: args.intervalCount }
108
+ : {}),
109
+ ...(args.trialDays !== undefined ? { trialDays: args.trialDays } : {}),
110
+ lastSyncedAt: Date.now(),
111
+ });
112
+
113
+ return result;
114
+ },
115
+ });
116
+
117
+ export const deletePlan = action({
118
+ args: {
119
+ credentials: epaycoCredentialsValidator,
120
+ epaycoPlanId: v.string(),
121
+ },
122
+ returns: v.any(),
123
+ handler: async (ctx, args) => {
124
+ const epayco = getEpaycoClient(args.credentials);
125
+ const result = unwrap(await epayco.plans.delete(args.epaycoPlanId));
126
+
127
+ await ctx.runMutation(internal.plans.upsertPlan, {
128
+ epaycoPlanId: args.epaycoPlanId,
129
+ status: "deleted",
130
+ lastSyncedAt: Date.now(),
131
+ });
132
+
133
+ return result;
134
+ },
135
+ });
@@ -0,0 +1,125 @@
1
+ import { v } from "convex/values";
2
+ import { action } from "./_generated/server.js";
3
+ import { internal } from "./_generated/api.js";
4
+ import { epaycoCredentialsValidator, pseInfoValidator } from "./validators.js";
5
+ import { getEpaycoClient, unwrap, dataOf, pick } from "./epaycoClient.js";
6
+ import { buildSplitPayload, storedReceivers } from "./payloads.js";
7
+ import { rateLimiter } from "./rateLimits.js";
8
+
9
+ /** Create a PSE (bank debit) transaction. Returns a `urlbanco` to redirect to. */
10
+ export const createPseTransaction = action({
11
+ args: {
12
+ credentials: epaycoCredentialsValidator,
13
+ userId: v.string(),
14
+ pseInfo: pseInfoValidator,
15
+ },
16
+ returns: v.any(),
17
+ handler: async (ctx, args) => {
18
+ await rateLimiter.limit(ctx, "createPseTransaction", {
19
+ key: args.userId,
20
+ throws: true,
21
+ });
22
+
23
+ const info = args.pseInfo;
24
+ const epayco = getEpaycoClient(args.credentials);
25
+
26
+ const result = unwrap(
27
+ await epayco.bank.create({
28
+ bank: info.bank,
29
+ invoice: info.bill,
30
+ description: info.description,
31
+ value: String(info.value),
32
+ tax: String(info.tax),
33
+ tax_base: String(info.taxBase),
34
+ currency: info.currency ?? "COP",
35
+ type_person: info.typePerson,
36
+ doc_type: info.docType,
37
+ doc_number: info.docNumber,
38
+ name: info.name,
39
+ last_name: info.lastName,
40
+ email: info.email,
41
+ country: info.country ?? "CO",
42
+ cell_phone: info.cellPhone,
43
+ ...(info.ip ? { ip: info.ip } : {}),
44
+ ...(info.urlResponse ? { url_response: info.urlResponse } : {}),
45
+ ...(info.urlConfirmation
46
+ ? { url_confirmation: info.urlConfirmation }
47
+ : {}),
48
+ metodoconfirmacion: "GET",
49
+ extra1: info.extra1 ?? "",
50
+ extra2: info.extra2 ?? "",
51
+ extra3: info.extra3 ?? "",
52
+ ...buildSplitPayload(info.split, true),
53
+ }),
54
+ );
55
+
56
+ const data = dataOf(result);
57
+ const refPayco = pick(data, ["ref_payco", "refPayco"]);
58
+
59
+ if (refPayco) {
60
+ await ctx.runMutation(internal.transactions.upsertTransaction, {
61
+ userId: args.userId,
62
+ epaycoRef: refPayco,
63
+ epaycoTransactionId: pick(data, [
64
+ "transactionID",
65
+ "transactionId",
66
+ "transaction_id",
67
+ ]),
68
+ paymentMethod: "pse",
69
+ status: "pending",
70
+ amount: info.value,
71
+ currency: info.currency ?? "COP",
72
+ description: info.description,
73
+ customerEmail: info.email,
74
+ bankName: pick(data, ["bank", "banco"]) ?? info.bank,
75
+ splitPayment: info.split !== undefined ? true : undefined,
76
+ splitReceivers: storedReceivers(info.split),
77
+ rawResponse: data,
78
+ lastSyncedAt: Date.now(),
79
+ });
80
+ }
81
+
82
+ return result;
83
+ },
84
+ });
85
+
86
+ /** Look up a PSE transaction's current state by its ticket id. */
87
+ export const getPseTransaction = action({
88
+ args: {
89
+ credentials: epaycoCredentialsValidator,
90
+ ticketId: v.string(),
91
+ },
92
+ returns: v.any(),
93
+ handler: async (_ctx, args) => {
94
+ const epayco = getEpaycoClient(args.credentials);
95
+ return unwrap(await epayco.bank.get(args.ticketId));
96
+ },
97
+ });
98
+
99
+ /** Fetch the PSE bank list and cache it locally. */
100
+ export const getBanks = action({
101
+ args: { credentials: epaycoCredentialsValidator },
102
+ returns: v.any(),
103
+ handler: async (ctx, args) => {
104
+ const epayco = getEpaycoClient(args.credentials);
105
+ const result = unwrap(await epayco.bank.getBanks());
106
+
107
+ const list = Array.isArray(result.data)
108
+ ? (result.data as Array<Record<string, unknown>>)
109
+ : [];
110
+
111
+ for (const bank of list) {
112
+ const bankCode = pick(bank, ["bankCode", "code", "value", "pseCode"]);
113
+ const bankName = pick(bank, ["bankName", "name", "description", "label"]);
114
+ if (bankCode && bankName) {
115
+ await ctx.runMutation(internal.banks.upsertBank, {
116
+ bankCode,
117
+ bankName,
118
+ lastSyncedAt: Date.now(),
119
+ });
120
+ }
121
+ }
122
+
123
+ return result;
124
+ },
125
+ });
@@ -0,0 +1,67 @@
1
+ import { RateLimiter, MINUTE } from "@convex-dev/rate-limiter";
2
+ import { components } from "./_generated/api.js";
3
+
4
+ export const rateLimiter = new RateLimiter(components.rateLimiter, {
5
+ createCustomer: {
6
+ kind: "token bucket",
7
+ rate: 5,
8
+ period: MINUTE,
9
+ capacity: 3,
10
+ },
11
+ createToken: {
12
+ kind: "token bucket",
13
+ rate: 10,
14
+ period: MINUTE,
15
+ capacity: 5,
16
+ },
17
+ createCharge: {
18
+ kind: "token bucket",
19
+ rate: 10,
20
+ period: MINUTE,
21
+ capacity: 5,
22
+ },
23
+ createSubscription: {
24
+ kind: "token bucket",
25
+ rate: 5,
26
+ period: MINUTE,
27
+ capacity: 3,
28
+ },
29
+ createPseTransaction: {
30
+ kind: "token bucket",
31
+ rate: 10,
32
+ period: MINUTE,
33
+ capacity: 5,
34
+ },
35
+ createCashPayment: {
36
+ kind: "token bucket",
37
+ rate: 10,
38
+ period: MINUTE,
39
+ capacity: 5,
40
+ },
41
+ createDaviplataPayment: {
42
+ kind: "token bucket",
43
+ rate: 10,
44
+ period: MINUTE,
45
+ capacity: 5,
46
+ },
47
+ // Keyed per ref_payco: caps OTP brute-force attempts on a single payment.
48
+ confirmDaviplataPayment: {
49
+ kind: "token bucket",
50
+ rate: 5,
51
+ period: MINUTE,
52
+ capacity: 5,
53
+ },
54
+ createSafetyPayPayment: {
55
+ kind: "token bucket",
56
+ rate: 10,
57
+ period: MINUTE,
58
+ capacity: 5,
59
+ },
60
+ chargeSubscription: {
61
+ kind: "token bucket",
62
+ rate: 5,
63
+ period: MINUTE,
64
+ capacity: 3,
65
+ },
66
+ webhookProcessing: { kind: "fixed window", rate: 200, period: MINUTE },
67
+ });
@@ -0,0 +1,78 @@
1
+ import { v } from "convex/values";
2
+ import { action } from "./_generated/server.js";
3
+ import { internal } from "./_generated/api.js";
4
+ import {
5
+ epaycoCredentialsValidator,
6
+ safetypayInfoValidator,
7
+ } from "./validators.js";
8
+ import { getEpaycoClient, unwrap, dataOf, pick } from "./epaycoClient.js";
9
+ import { rateLimiter } from "./rateLimits.js";
10
+
11
+ /** Create a SafetyPay transaction (cash or online bank). */
12
+ export const createSafetyPayPayment = action({
13
+ args: {
14
+ credentials: epaycoCredentialsValidator,
15
+ userId: v.string(),
16
+ safetypayInfo: safetypayInfoValidator,
17
+ },
18
+ returns: v.any(),
19
+ handler: async (ctx, args) => {
20
+ await rateLimiter.limit(ctx, "createSafetyPayPayment", {
21
+ key: args.userId,
22
+ throws: true,
23
+ });
24
+
25
+ const info = args.safetypayInfo;
26
+ const epayco = getEpaycoClient(args.credentials);
27
+
28
+ const result = unwrap(
29
+ await epayco.safetypay.create({
30
+ cash: info.cash,
31
+ ...(info.endDate ? { end_date: info.endDate } : {}),
32
+ doc_type: info.docType,
33
+ doc_number: info.docNumber,
34
+ name: info.name,
35
+ last_name: info.lastName,
36
+ email: info.email,
37
+ ind_country: info.indCountry ?? "57",
38
+ phone: info.phone,
39
+ country: info.country ?? "CO",
40
+ ...(info.invoice ? { invoice: info.invoice } : {}),
41
+ ...(info.city ? { city: info.city } : {}),
42
+ ...(info.address ? { address: info.address } : {}),
43
+ ...(info.ip ? { ip: info.ip } : {}),
44
+ currency: info.currency ?? "COP",
45
+ description: info.description,
46
+ value: info.value,
47
+ tax: info.tax,
48
+ ico: info.ico ?? 0,
49
+ tax_base: info.taxBase,
50
+ method_confirmation: info.methodConfirmation ?? "",
51
+ ...(info.urlConfirmation
52
+ ? { url_confirmation: info.urlConfirmation }
53
+ : {}),
54
+ }),
55
+ );
56
+
57
+ const data = dataOf(result);
58
+ const refPayco = pick(data, ["ref_payco", "refPayco"]);
59
+
60
+ if (refPayco) {
61
+ await ctx.runMutation(internal.transactions.upsertTransaction, {
62
+ userId: args.userId,
63
+ epaycoRef: refPayco,
64
+ epaycoTransactionId: pick(data, ["transactionId", "transaction_id"]),
65
+ paymentMethod: "safetypay",
66
+ status: "pending",
67
+ amount: info.value,
68
+ currency: info.currency ?? "COP",
69
+ description: info.description,
70
+ customerEmail: info.email,
71
+ rawResponse: data,
72
+ lastSyncedAt: Date.now(),
73
+ });
74
+ }
75
+
76
+ return result;
77
+ },
78
+ });