@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 +4 -0
- package/Dockerfile +41 -0
- package/README.md +248 -0
- package/dist/activities/index.d.ts +2 -0
- package/dist/activities/index.js +19 -0
- package/dist/chunk-AV4OEJXY.js +132 -0
- package/dist/chunk-CZTZBCNV.js +133 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-X2Y2ZUQA.js +297 -0
- package/dist/index-CXmmtGo_.d.ts +25 -0
- package/dist/index-CsnpS83V.d.ts +72 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +21 -0
- package/dist/workflows/index.d.ts +27 -0
- package/dist/workflows/index.js +13 -0
- package/package.json +54 -0
- package/src/activities/index.ts +224 -0
- package/src/index.ts +22 -0
- package/src/lib/crypto.ts +24 -0
- package/src/lib/db.ts +91 -0
- package/src/lib/observability/index.ts +2 -0
- package/src/lib/observability/logger.ts +44 -0
- package/src/lib/observability/otel.ts +53 -0
- package/src/lib/registry.ts +28 -0
- package/src/types/index.ts +95 -0
- package/src/worker.ts +105 -0
- package/src/workflows/index.ts +5 -0
- package/src/workflows/payment.ts +111 -0
- package/src/workflows/webhook.ts +64 -0
- package/tsconfig.json +20 -0
package/.env.example
ADDED
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,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
|
+
};
|