@payloops/processor-core 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/.env.example ADDED
@@ -0,0 +1,4 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/loop
2
+ ENCRYPTION_KEY=your-encryption-key-at-least-32-chars
3
+ TEMPORAL_ADDRESS=localhost:7233
4
+ TEMPORAL_NAMESPACE=loop
package/Dockerfile ADDED
@@ -0,0 +1,41 @@
1
+ # Build stage
2
+ FROM node:22-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package files
7
+ COPY package.json package-lock.json ./
8
+
9
+ # Install dependencies
10
+ RUN npm ci
11
+
12
+ # Copy source
13
+ COPY src ./src
14
+ COPY tsconfig.json ./
15
+
16
+ # Build
17
+ RUN npm run build
18
+
19
+ # Production stage
20
+ FROM node:22-alpine AS runner
21
+
22
+ WORKDIR /app
23
+
24
+ # Copy package files
25
+ COPY package.json package-lock.json ./
26
+
27
+ # Install production dependencies only
28
+ RUN npm ci --omit=dev
29
+
30
+ # Copy built files
31
+ COPY --from=builder /app/dist ./dist
32
+
33
+ # Create non-root user
34
+ RUN addgroup --system --gid 1001 nodejs && \
35
+ adduser --system --uid 1001 loop
36
+
37
+ USER loop
38
+
39
+ ENV NODE_ENV=production
40
+
41
+ CMD ["node", "dist/worker.js"]
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # PayLoops Processor Core
2
+
3
+ The **processor-core** service is the workflow engine powering PayLoops. It orchestrates payment processing, handles retries, manages state transitions, and ensures reliable webhook delivery—all with durability guarantees from [Temporal](https://temporal.io).
4
+
5
+ ## Role in the Platform
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────────┐
9
+ │ │
10
+ │ Backend API │
11
+ │ │ │
12
+ │ │ Triggers workflows │
13
+ │ ▼ │
14
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
15
+ │ │ ★ PROCESSOR-CORE (this repo) ★ │ │
16
+ │ │ │ │
17
+ │ │ ┌──────────────────────────────────────────────────────────┐ │ │
18
+ │ │ │ Temporal Workers │ │ │
19
+ │ │ │ │ │ │
20
+ │ │ │ PaymentWorkflow RefundWorkflow WebhookWorkflow │ │ │
21
+ │ │ │ │ │ │ │ │ │
22
+ │ │ │ ▼ ▼ ▼ │ │ │
23
+ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
24
+ │ │ │ │Activities│ │Activities│ │Activities│ │ │ │
25
+ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
26
+ │ │ └──────────────────────────────────────────────────────────┘ │ │
27
+ │ │ │ │ │
28
+ │ └───────────────────────────────┼──────────────────────────────────┘ │
29
+ │ │ │
30
+ │ ▼ │
31
+ │ ┌────────────────────────────────────────┐ │
32
+ │ │ Payment Processors │ │
33
+ │ │ processor-stripe processor-razorpay │ │
34
+ │ └────────────────────────────────────────┘ │
35
+ │ │
36
+ └─────────────────────────────────────────────────────────────────────────┘
37
+ ```
38
+
39
+ ## Why Temporal?
40
+
41
+ Payment processing requires **durability** and **reliability**:
42
+
43
+ - If the server crashes mid-payment, the workflow resumes exactly where it left off
44
+ - Long-running operations (like waiting for 3DS) don't block resources
45
+ - Automatic retries with configurable backoff
46
+ - Full audit trail of every state transition
47
+ - Easy to add timeouts, deadlines, and cancellation
48
+
49
+ ## Workflows
50
+
51
+ ### PaymentWorkflow
52
+
53
+ Handles the complete lifecycle of a payment from order creation to completion.
54
+
55
+ ```
56
+ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
57
+ │ pending │───▶│ routing │───▶│charging │───▶│ 3DS/SCA │───▶│completed│
58
+ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
59
+
60
+
61
+ Wait for signal
62
+ (threeDSComplete)
63
+ ```
64
+
65
+ **States:**
66
+ - `pending` - Order created, awaiting payment
67
+ - `processing` - Routing to optimal processor
68
+ - `authorized` - Payment authorized, awaiting capture
69
+ - `requires_action` - Waiting for 3DS/SCA verification
70
+ - `captured` - Payment successfully captured
71
+ - `failed` - Payment failed
72
+
73
+ **Signals:**
74
+ - `threeDSComplete` - Customer completed 3DS challenge
75
+
76
+ ### WebhookDeliveryWorkflow
77
+
78
+ Ensures merchants receive webhook notifications reliably.
79
+
80
+ ```
81
+ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
82
+ │ attempt │───▶│ retry │───▶│ retry │───▶│ final │
83
+ │ #1 │ │ #2 │ │ #3 │ │ status │
84
+ └─────────┘ └─────────┘ └─────────┘ └─────────┘
85
+ 1min 5min 30min
86
+ ```
87
+
88
+ **Retry Schedule:**
89
+ 1. Immediate
90
+ 2. 1 minute
91
+ 3. 5 minutes
92
+ 4. 30 minutes
93
+ 5. 2 hours
94
+ 6. 24 hours (final attempt)
95
+
96
+ ## Processor Registry
97
+
98
+ The core provides a plugin system for payment processors:
99
+
100
+ ```typescript
101
+ // In processor-stripe or processor-razorpay
102
+ import { registerProcessor, PaymentProcessor } from '@payloops/processor-core';
103
+
104
+ class StripeProcessor implements PaymentProcessor {
105
+ name = 'stripe';
106
+
107
+ async createPayment(input, config) { /* ... */ }
108
+ async capturePayment(orderId, amount, config) { /* ... */ }
109
+ async refundPayment(transactionId, amount, config) { /* ... */ }
110
+ async getPaymentStatus(orderId, config) { /* ... */ }
111
+ }
112
+
113
+ registerProcessor(new StripeProcessor());
114
+ ```
115
+
116
+ Workflows dynamically load the appropriate processor based on routing rules.
117
+
118
+ ## Activities
119
+
120
+ Activities are the building blocks that workflows orchestrate:
121
+
122
+ | Activity | Description |
123
+ |----------|-------------|
124
+ | `getOrder` | Fetch order details from database |
125
+ | `updateOrderStatus` | Update order status in database |
126
+ | `getProcessorConfig` | Get decrypted processor credentials |
127
+ | `routePayment` | Determine which processor to use |
128
+ | `processPayment` | Execute payment via processor |
129
+ | `deliverWebhook` | POST webhook to merchant endpoint |
130
+
131
+ ## Development
132
+
133
+ ### Prerequisites
134
+
135
+ - Node.js 22+
136
+ - pnpm
137
+ - Temporal server (via Docker)
138
+ - PostgreSQL (via Docker)
139
+
140
+ ### Setup
141
+
142
+ ```bash
143
+ # Install dependencies
144
+ pnpm install
145
+
146
+ # Copy environment template
147
+ cp .env.example .env
148
+
149
+ # Start Temporal (from root loop repo)
150
+ docker-compose up -d temporal
151
+
152
+ # Start worker in development mode
153
+ pnpm dev
154
+ ```
155
+
156
+ ### Available Scripts
157
+
158
+ | Command | Description |
159
+ |---------|-------------|
160
+ | `pnpm dev` | Start worker with hot reload |
161
+ | `pnpm build` | Build for production |
162
+ | `pnpm start` | Run production worker |
163
+ | `pnpm typecheck` | Run TypeScript compiler |
164
+ | `pnpm lint` | Run ESLint |
165
+
166
+ ## Configuration
167
+
168
+ | Variable | Required | Description |
169
+ |----------|----------|-------------|
170
+ | `DATABASE_URL` | Yes | PostgreSQL connection string |
171
+ | `TEMPORAL_ADDRESS` | Yes | Temporal server (default: localhost:7233) |
172
+ | `TEMPORAL_NAMESPACE` | Yes | Temporal namespace |
173
+ | `ENCRYPTION_KEY` | Yes | Key to decrypt processor credentials |
174
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | No | OpenTelemetry collector endpoint (default: http://localhost:4318) |
175
+ | `OTEL_SERVICE_NAME` | No | Service name for telemetry (default: loop-processor-core) |
176
+
177
+ ## Adding a New Processor
178
+
179
+ 1. Create a new repository (e.g., `processor-paypal`)
180
+ 2. Implement the `PaymentProcessor` interface
181
+ 3. Call `registerProcessor()` on module load
182
+ 4. Import the processor in this repo's worker
183
+
184
+ See [processor-stripe](https://github.com/payloops/processor-stripe) for a complete example.
185
+
186
+ ## Observability
187
+
188
+ The processor-core is fully instrumented with OpenTelemetry for distributed tracing, metrics, and structured logging.
189
+
190
+ ### What's Collected
191
+
192
+ | Type | Description |
193
+ |------|-------------|
194
+ | **Traces** | Activity spans with workflow context, processor calls |
195
+ | **Metrics** | Activity execution counts, durations, error rates |
196
+ | **Logs** | Structured JSON logs with `trace_id`, `span_id`, workflow context |
197
+
198
+ ### Activity Tracing
199
+
200
+ Every activity execution is automatically traced with:
201
+
202
+ ```
203
+ activity.processPayment
204
+ ├── temporal.activity.type: processPayment
205
+ ├── temporal.workflow.id: payment-ord_abc123
206
+ ├── temporal.task_queue: payment-queue
207
+ └── duration: 245ms
208
+ ```
209
+
210
+ ### Correlation with Backend
211
+
212
+ When the backend triggers a workflow, the correlation ID is propagated via:
213
+ - **Search Attributes**: `CorrelationId`, `MerchantId`, `OrderId`
214
+ - **Memo**: `traceContext`, `correlationId`
215
+
216
+ This enables end-to-end tracing from HTTP request → workflow → activities.
217
+
218
+ ### Viewing in OpenObserve
219
+
220
+ 1. Open http://localhost:5080 (login: `admin@loop.dev` / `admin123`)
221
+ 2. **Logs**: Filter by `service: loop-processor-core` or workflow ID
222
+ 3. **Traces**: View activity spans linked to parent workflow
223
+ 4. **Metrics**: Query activity execution metrics
224
+
225
+ ### Temporal UI
226
+
227
+ Access at `http://localhost:8080` to:
228
+ - View running and completed workflows
229
+ - Inspect workflow history and state
230
+ - Search by `CorrelationId` search attribute
231
+ - Manually trigger signals
232
+ - Terminate stuck workflows
233
+
234
+ ### Workflow IDs
235
+
236
+ Workflows use predictable IDs for easy lookup:
237
+ - Payment: `payment-{orderId}`
238
+ - Webhook: `webhook-{eventId}`
239
+
240
+ ## Related Repositories
241
+
242
+ - [backend](https://github.com/payloops/backend) - Triggers workflows via Temporal client
243
+ - [processor-stripe](https://github.com/payloops/processor-stripe) - Stripe processor implementation
244
+ - [processor-razorpay](https://github.com/payloops/processor-razorpay) - Razorpay processor implementation
245
+
246
+ ## License
247
+
248
+ Copyright © 2025 PayLoops. All rights reserved.
@@ -0,0 +1,2 @@
1
+ import '../index-CsnpS83V.js';
2
+ export { c as capturePayment, d as deliverWebhook, a as getMerchantWebhookUrl, g as getProcessorConfig, p as processPayment, r as refundPayment, u as updateOrderStatus } from '../index-CXmmtGo_.js';
@@ -0,0 +1,19 @@
1
+ import {
2
+ capturePayment,
3
+ deliverWebhook,
4
+ getMerchantWebhookUrl,
5
+ getProcessorConfig,
6
+ processPayment,
7
+ refundPayment,
8
+ updateOrderStatus
9
+ } from "../chunk-X2Y2ZUQA.js";
10
+ import "../chunk-MLKGABMK.js";
11
+ export {
12
+ capturePayment,
13
+ deliverWebhook,
14
+ getMerchantWebhookUrl,
15
+ getProcessorConfig,
16
+ processPayment,
17
+ refundPayment,
18
+ updateOrderStatus
19
+ };
@@ -0,0 +1,132 @@
1
+ // src/workflows/payment.ts
2
+ import { proxyActivities, sleep, defineSignal, setHandler } from "@temporalio/workflow";
3
+ var { processPayment, updateOrderStatus, capturePayment } = proxyActivities({
4
+ startToCloseTimeout: "2 minutes",
5
+ retry: {
6
+ initialInterval: "1s",
7
+ maximumInterval: "1m",
8
+ backoffCoefficient: 2,
9
+ maximumAttempts: 5
10
+ }
11
+ });
12
+ var completePaymentSignal = defineSignal(
13
+ "completePayment"
14
+ );
15
+ var cancelPaymentSignal = defineSignal("cancelPayment");
16
+ async function PaymentWorkflow(input) {
17
+ let paymentCompleted = false;
18
+ let paymentResult = null;
19
+ let cancelled = false;
20
+ setHandler(completePaymentSignal, (result) => {
21
+ paymentCompleted = true;
22
+ paymentResult = {
23
+ success: result.success,
24
+ status: result.success ? "captured" : "failed",
25
+ processorTransactionId: result.processorTransactionId
26
+ };
27
+ });
28
+ setHandler(cancelPaymentSignal, () => {
29
+ cancelled = true;
30
+ });
31
+ try {
32
+ const result = await processPayment({
33
+ orderId: input.orderId,
34
+ merchantId: input.merchantId,
35
+ amount: input.amount,
36
+ currency: input.currency,
37
+ processor: input.processor,
38
+ returnUrl: input.returnUrl
39
+ });
40
+ if (result.status === "requires_action") {
41
+ await updateOrderStatus(input.orderId, "requires_action", result.processorOrderId);
42
+ const timeout = 15 * 60 * 1e3;
43
+ const startTime = Date.now();
44
+ while (!paymentCompleted && !cancelled && Date.now() - startTime < timeout) {
45
+ await sleep("10 seconds");
46
+ }
47
+ if (cancelled) {
48
+ await updateOrderStatus(input.orderId, "cancelled");
49
+ return { success: false, status: "failed", errorCode: "cancelled", errorMessage: "Payment cancelled" };
50
+ }
51
+ if (!paymentCompleted) {
52
+ await updateOrderStatus(input.orderId, "failed");
53
+ return { success: false, status: "failed", errorCode: "timeout", errorMessage: "Payment timeout" };
54
+ }
55
+ if (paymentResult) {
56
+ await updateOrderStatus(
57
+ input.orderId,
58
+ paymentResult.status,
59
+ result.processorOrderId,
60
+ paymentResult.processorTransactionId
61
+ );
62
+ return paymentResult;
63
+ }
64
+ }
65
+ await updateOrderStatus(input.orderId, result.status, result.processorOrderId, result.processorTransactionId);
66
+ return result;
67
+ } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
69
+ await updateOrderStatus(input.orderId, "failed");
70
+ return {
71
+ success: false,
72
+ status: "failed",
73
+ errorCode: "workflow_error",
74
+ errorMessage
75
+ };
76
+ }
77
+ }
78
+
79
+ // src/workflows/webhook.ts
80
+ import { proxyActivities as proxyActivities2, sleep as sleep2 } from "@temporalio/workflow";
81
+ var { deliverWebhook, getMerchantWebhookUrl } = proxyActivities2({
82
+ startToCloseTimeout: "1 minute",
83
+ retry: {
84
+ initialInterval: "1s",
85
+ maximumInterval: "30s",
86
+ backoffCoefficient: 2,
87
+ maximumAttempts: 3
88
+ }
89
+ });
90
+ var MAX_ATTEMPTS = 5;
91
+ var RETRY_DELAYS = [
92
+ 60 * 1e3,
93
+ // 1 minute
94
+ 5 * 60 * 1e3,
95
+ // 5 minutes
96
+ 30 * 60 * 1e3,
97
+ // 30 minutes
98
+ 2 * 60 * 60 * 1e3,
99
+ // 2 hours
100
+ 24 * 60 * 60 * 1e3
101
+ // 24 hours
102
+ ];
103
+ async function WebhookDeliveryWorkflow(input) {
104
+ const { secret } = await getMerchantWebhookUrl(input.merchantId);
105
+ let attempt = 0;
106
+ let lastResult = null;
107
+ while (attempt < MAX_ATTEMPTS) {
108
+ attempt++;
109
+ const result = await deliverWebhook(input.webhookEventId, input.webhookUrl, secret || void 0, input.payload);
110
+ lastResult = result;
111
+ if (result.success) {
112
+ return result;
113
+ }
114
+ if (attempt >= MAX_ATTEMPTS) {
115
+ return {
116
+ success: false,
117
+ attempts: attempt,
118
+ errorMessage: result.errorMessage || "Max attempts reached"
119
+ };
120
+ }
121
+ const delay = RETRY_DELAYS[attempt - 1] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
122
+ await sleep2(delay);
123
+ }
124
+ return lastResult || { success: false, attempts: attempt, errorMessage: "Unknown error" };
125
+ }
126
+
127
+ export {
128
+ completePaymentSignal,
129
+ cancelPaymentSignal,
130
+ PaymentWorkflow,
131
+ WebhookDeliveryWorkflow
132
+ };
@@ -0,0 +1,133 @@
1
+ // src/workflows/payment.ts
2
+ import { proxyActivities, sleep, defineSignal, setHandler } from "@temporalio/workflow";
3
+ var { processPayment, updateOrderStatus, capturePayment } = proxyActivities({
4
+ startToCloseTimeout: "2 minutes",
5
+ retry: {
6
+ initialInterval: "1s",
7
+ maximumInterval: "1m",
8
+ backoffCoefficient: 2,
9
+ maximumAttempts: 5
10
+ }
11
+ });
12
+ var completePaymentSignal = defineSignal(
13
+ "completePayment"
14
+ );
15
+ var cancelPaymentSignal = defineSignal("cancelPayment");
16
+ async function PaymentWorkflow(input) {
17
+ let paymentCompleted = false;
18
+ let paymentResult = null;
19
+ let cancelled = false;
20
+ setHandler(completePaymentSignal, (result) => {
21
+ paymentCompleted = true;
22
+ paymentResult = {
23
+ success: result.success,
24
+ status: result.success ? "captured" : "failed",
25
+ processorTransactionId: result.processorTransactionId
26
+ };
27
+ });
28
+ setHandler(cancelPaymentSignal, () => {
29
+ cancelled = true;
30
+ });
31
+ try {
32
+ const result = await processPayment({
33
+ orderId: input.orderId,
34
+ merchantId: input.merchantId,
35
+ amount: input.amount,
36
+ currency: input.currency,
37
+ processor: input.processor,
38
+ returnUrl: input.returnUrl
39
+ });
40
+ if (result.status === "requires_action") {
41
+ await updateOrderStatus(input.orderId, "requires_action", result.processorOrderId);
42
+ const timeout = 15 * 60 * 1e3;
43
+ const startTime = Date.now();
44
+ while (!paymentCompleted && !cancelled && Date.now() - startTime < timeout) {
45
+ await sleep("10 seconds");
46
+ }
47
+ if (cancelled) {
48
+ await updateOrderStatus(input.orderId, "cancelled");
49
+ return { success: false, status: "failed", errorCode: "cancelled", errorMessage: "Payment cancelled" };
50
+ }
51
+ if (!paymentCompleted) {
52
+ await updateOrderStatus(input.orderId, "failed");
53
+ return { success: false, status: "failed", errorCode: "timeout", errorMessage: "Payment timeout" };
54
+ }
55
+ if (paymentResult !== null) {
56
+ const finalResult = paymentResult;
57
+ await updateOrderStatus(
58
+ input.orderId,
59
+ finalResult.status,
60
+ result.processorOrderId,
61
+ finalResult.processorTransactionId
62
+ );
63
+ return finalResult;
64
+ }
65
+ }
66
+ await updateOrderStatus(input.orderId, result.status, result.processorOrderId, result.processorTransactionId);
67
+ return result;
68
+ } catch (error) {
69
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
70
+ await updateOrderStatus(input.orderId, "failed");
71
+ return {
72
+ success: false,
73
+ status: "failed",
74
+ errorCode: "workflow_error",
75
+ errorMessage
76
+ };
77
+ }
78
+ }
79
+
80
+ // src/workflows/webhook.ts
81
+ import { proxyActivities as proxyActivities2, sleep as sleep2 } from "@temporalio/workflow";
82
+ var { deliverWebhook, getMerchantWebhookUrl } = proxyActivities2({
83
+ startToCloseTimeout: "1 minute",
84
+ retry: {
85
+ initialInterval: "1s",
86
+ maximumInterval: "30s",
87
+ backoffCoefficient: 2,
88
+ maximumAttempts: 3
89
+ }
90
+ });
91
+ var MAX_ATTEMPTS = 5;
92
+ var RETRY_DELAYS = [
93
+ 60 * 1e3,
94
+ // 1 minute
95
+ 5 * 60 * 1e3,
96
+ // 5 minutes
97
+ 30 * 60 * 1e3,
98
+ // 30 minutes
99
+ 2 * 60 * 60 * 1e3,
100
+ // 2 hours
101
+ 24 * 60 * 60 * 1e3
102
+ // 24 hours
103
+ ];
104
+ async function WebhookDeliveryWorkflow(input) {
105
+ const { secret } = await getMerchantWebhookUrl(input.merchantId);
106
+ let attempt = 0;
107
+ let lastResult = null;
108
+ while (attempt < MAX_ATTEMPTS) {
109
+ attempt++;
110
+ const result = await deliverWebhook(input.webhookEventId, input.webhookUrl, secret || void 0, input.payload);
111
+ lastResult = result;
112
+ if (result.success) {
113
+ return result;
114
+ }
115
+ if (attempt >= MAX_ATTEMPTS) {
116
+ return {
117
+ success: false,
118
+ attempts: attempt,
119
+ errorMessage: result.errorMessage || "Max attempts reached"
120
+ };
121
+ }
122
+ const delay = RETRY_DELAYS[attempt - 1] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
123
+ await sleep2(delay);
124
+ }
125
+ return lastResult || { success: false, attempts: attempt, errorMessage: "Unknown error" };
126
+ }
127
+
128
+ export {
129
+ completePaymentSignal,
130
+ cancelPaymentSignal,
131
+ PaymentWorkflow,
132
+ WebhookDeliveryWorkflow
133
+ };
@@ -0,0 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };