@payloops/processor-core 0.0.1 → 0.0.8
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/dist/index.d.ts +70 -11
- package/dist/index.js +0 -21
- package/dist/lib/observability/index.d.ts +1 -0
- package/dist/lib/observability/index.js +2 -0
- package/package.json +17 -30
- package/.env.example +0 -4
- package/Dockerfile +0 -41
- package/dist/activities/index.d.ts +0 -2
- package/dist/activities/index.js +0 -19
- package/dist/chunk-AV4OEJXY.js +0 -132
- package/dist/chunk-CZTZBCNV.js +0 -133
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-X2Y2ZUQA.js +0 -297
- package/dist/index-CXmmtGo_.d.ts +0 -25
- package/dist/index-CsnpS83V.d.ts +0 -72
- package/dist/workflows/index.d.ts +0 -27
- package/dist/workflows/index.js +0 -13
- package/src/activities/index.ts +0 -224
- package/src/index.ts +0 -22
- package/src/lib/crypto.ts +0 -24
- package/src/lib/db.ts +0 -91
- package/src/lib/observability/index.ts +0 -2
- package/src/lib/observability/logger.ts +0 -44
- package/src/lib/observability/otel.ts +0 -53
- package/src/lib/registry.ts +0 -28
- package/src/types/index.ts +0 -95
- package/src/worker.ts +0 -105
- package/src/workflows/index.ts +0 -5
- package/src/workflows/payment.ts +0 -111
- package/src/workflows/webhook.ts +0 -64
- package/tsconfig.json +0 -20
package/src/lib/crypto.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
2
|
-
|
|
3
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
-
|
|
5
|
-
function getKey(): Buffer {
|
|
6
|
-
const key = process.env.ENCRYPTION_KEY;
|
|
7
|
-
if (!key) throw new Error('ENCRYPTION_KEY not set');
|
|
8
|
-
return createHash('sha256').update(key).digest();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function decrypt(encryptedText: string): string {
|
|
12
|
-
const [ivHex, authTagHex, encrypted] = encryptedText.split(':');
|
|
13
|
-
|
|
14
|
-
const iv = Buffer.from(ivHex, 'hex');
|
|
15
|
-
const authTag = Buffer.from(authTagHex, 'hex');
|
|
16
|
-
const decipher = createDecipheriv(ALGORITHM, getKey(), iv);
|
|
17
|
-
|
|
18
|
-
decipher.setAuthTag(authTag);
|
|
19
|
-
|
|
20
|
-
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
21
|
-
decrypted += decipher.final('utf8');
|
|
22
|
-
|
|
23
|
-
return decrypted;
|
|
24
|
-
}
|
package/src/lib/db.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
-
import { Pool } from 'pg';
|
|
3
|
-
import {
|
|
4
|
-
pgTable,
|
|
5
|
-
text,
|
|
6
|
-
timestamp,
|
|
7
|
-
integer,
|
|
8
|
-
boolean,
|
|
9
|
-
jsonb,
|
|
10
|
-
varchar,
|
|
11
|
-
uuid
|
|
12
|
-
} from 'drizzle-orm/pg-core';
|
|
13
|
-
|
|
14
|
-
// Schema definitions (shared with backend)
|
|
15
|
-
export const orders = pgTable('orders', {
|
|
16
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
17
|
-
merchantId: uuid('merchant_id').notNull(),
|
|
18
|
-
externalId: varchar('external_id', { length: 255 }).notNull(),
|
|
19
|
-
amount: integer('amount').notNull(),
|
|
20
|
-
currency: varchar('currency', { length: 3 }).notNull().default('USD'),
|
|
21
|
-
status: varchar('status', { length: 50 }).notNull().default('pending'),
|
|
22
|
-
processor: varchar('processor', { length: 50 }),
|
|
23
|
-
processorOrderId: varchar('processor_order_id', { length: 255 }),
|
|
24
|
-
metadata: jsonb('metadata').default({}),
|
|
25
|
-
workflowId: varchar('workflow_id', { length: 255 }),
|
|
26
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
27
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export const transactions = pgTable('transactions', {
|
|
31
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
32
|
-
orderId: uuid('order_id').notNull(),
|
|
33
|
-
type: varchar('type', { length: 50 }).notNull(),
|
|
34
|
-
amount: integer('amount').notNull(),
|
|
35
|
-
status: varchar('status', { length: 50 }).notNull().default('pending'),
|
|
36
|
-
processorTransactionId: varchar('processor_transaction_id', { length: 255 }),
|
|
37
|
-
processorResponse: jsonb('processor_response'),
|
|
38
|
-
errorCode: varchar('error_code', { length: 100 }),
|
|
39
|
-
errorMessage: text('error_message'),
|
|
40
|
-
createdAt: timestamp('created_at').defaultNow().notNull()
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export const processorConfigs = pgTable('processor_configs', {
|
|
44
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
45
|
-
merchantId: uuid('merchant_id').notNull(),
|
|
46
|
-
processor: varchar('processor', { length: 50 }).notNull(),
|
|
47
|
-
credentialsEncrypted: text('credentials_encrypted').notNull(),
|
|
48
|
-
priority: integer('priority').notNull().default(1),
|
|
49
|
-
enabled: boolean('enabled').notNull().default(true),
|
|
50
|
-
testMode: boolean('test_mode').notNull().default(true),
|
|
51
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
52
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export const webhookEvents = pgTable('webhook_events', {
|
|
56
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
57
|
-
merchantId: uuid('merchant_id').notNull(),
|
|
58
|
-
orderId: uuid('order_id'),
|
|
59
|
-
eventType: varchar('event_type', { length: 100 }).notNull(),
|
|
60
|
-
payload: jsonb('payload').notNull(),
|
|
61
|
-
status: varchar('status', { length: 50 }).notNull().default('pending'),
|
|
62
|
-
attempts: integer('attempts').notNull().default(0),
|
|
63
|
-
lastAttemptAt: timestamp('last_attempt_at'),
|
|
64
|
-
nextRetryAt: timestamp('next_retry_at'),
|
|
65
|
-
deliveredAt: timestamp('delivered_at'),
|
|
66
|
-
workflowId: varchar('workflow_id', { length: 255 }),
|
|
67
|
-
createdAt: timestamp('created_at').defaultNow().notNull()
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
export const merchants = pgTable('merchants', {
|
|
71
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
72
|
-
name: varchar('name', { length: 255 }).notNull(),
|
|
73
|
-
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
74
|
-
webhookUrl: text('webhook_url'),
|
|
75
|
-
webhookSecret: text('webhook_secret')
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Database connection
|
|
79
|
-
let dbInstance: ReturnType<typeof drizzle> | null = null;
|
|
80
|
-
|
|
81
|
-
export function getDb() {
|
|
82
|
-
if (!dbInstance) {
|
|
83
|
-
const pool = new Pool({
|
|
84
|
-
connectionString: process.env.DATABASE_URL
|
|
85
|
-
});
|
|
86
|
-
dbInstance = drizzle(pool);
|
|
87
|
-
}
|
|
88
|
-
return dbInstance;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export type Database = ReturnType<typeof getDb>;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import pino from 'pino';
|
|
2
|
-
import { trace } from '@opentelemetry/api';
|
|
3
|
-
|
|
4
|
-
const NODE_ENV = process.env.NODE_ENV || 'development';
|
|
5
|
-
const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || 'loop-processor-core';
|
|
6
|
-
|
|
7
|
-
const traceMixin = () => {
|
|
8
|
-
const span = trace.getActiveSpan();
|
|
9
|
-
if (!span) return {};
|
|
10
|
-
|
|
11
|
-
const spanContext = span.spanContext();
|
|
12
|
-
return {
|
|
13
|
-
trace_id: spanContext.traceId,
|
|
14
|
-
span_id: spanContext.spanId
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const logger = pino({
|
|
19
|
-
level: NODE_ENV === 'production' ? 'info' : 'debug',
|
|
20
|
-
mixin: traceMixin,
|
|
21
|
-
base: {
|
|
22
|
-
service: SERVICE_NAME,
|
|
23
|
-
env: NODE_ENV
|
|
24
|
-
},
|
|
25
|
-
timestamp: pino.stdTimeFunctions.isoTime,
|
|
26
|
-
transport:
|
|
27
|
-
NODE_ENV !== 'production'
|
|
28
|
-
? { target: 'pino-pretty', options: { colorize: true } }
|
|
29
|
-
: undefined
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export function createActivityLogger(activityName: string, correlationId?: string) {
|
|
33
|
-
return logger.child({
|
|
34
|
-
activity: activityName,
|
|
35
|
-
correlationId
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createWorkflowLogger(workflowId: string, correlationId?: string) {
|
|
40
|
-
return logger.child({
|
|
41
|
-
workflowId,
|
|
42
|
-
correlationId
|
|
43
|
-
});
|
|
44
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
2
|
-
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
3
|
-
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
4
|
-
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
5
|
-
import { Resource } from '@opentelemetry/resources';
|
|
6
|
-
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
7
|
-
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
8
|
-
|
|
9
|
-
let sdk: NodeSDK | null = null;
|
|
10
|
-
|
|
11
|
-
export function initTelemetry(serviceName: string, serviceVersion = '0.0.1'): NodeSDK {
|
|
12
|
-
if (sdk) return sdk;
|
|
13
|
-
|
|
14
|
-
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318';
|
|
15
|
-
|
|
16
|
-
sdk = new NodeSDK({
|
|
17
|
-
resource: new Resource({
|
|
18
|
-
[ATTR_SERVICE_NAME]: serviceName,
|
|
19
|
-
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
20
|
-
'deployment.environment': process.env.NODE_ENV || 'development'
|
|
21
|
-
}),
|
|
22
|
-
|
|
23
|
-
traceExporter: new OTLPTraceExporter({
|
|
24
|
-
url: `${otlpEndpoint}/v1/traces`
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
metricReader: new PeriodicExportingMetricReader({
|
|
28
|
-
exporter: new OTLPMetricExporter({
|
|
29
|
-
url: `${otlpEndpoint}/v1/metrics`
|
|
30
|
-
}),
|
|
31
|
-
exportIntervalMillis: 30000
|
|
32
|
-
}),
|
|
33
|
-
|
|
34
|
-
instrumentations: [
|
|
35
|
-
getNodeAutoInstrumentations({
|
|
36
|
-
'@opentelemetry/instrumentation-fs': { enabled: false },
|
|
37
|
-
'@opentelemetry/instrumentation-http': { enabled: true },
|
|
38
|
-
'@opentelemetry/instrumentation-pg': { enabled: true }
|
|
39
|
-
})
|
|
40
|
-
]
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
sdk.start();
|
|
44
|
-
|
|
45
|
-
process.on('SIGTERM', () => {
|
|
46
|
-
sdk
|
|
47
|
-
?.shutdown()
|
|
48
|
-
.then(() => console.log('Telemetry shut down'))
|
|
49
|
-
.catch((err) => console.error('Telemetry shutdown error', err));
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return sdk;
|
|
53
|
-
}
|
package/src/lib/registry.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { PaymentProcessor, ProcessorRegistry } from '../types';
|
|
2
|
-
|
|
3
|
-
// Global processor registry
|
|
4
|
-
const processors: ProcessorRegistry = new Map();
|
|
5
|
-
|
|
6
|
-
export function registerProcessor(processor: PaymentProcessor): void {
|
|
7
|
-
if (processors.has(processor.name)) {
|
|
8
|
-
throw new Error(`Processor ${processor.name} is already registered`);
|
|
9
|
-
}
|
|
10
|
-
processors.set(processor.name, processor);
|
|
11
|
-
console.log(`Registered processor: ${processor.name}`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getProcessor(name: string): PaymentProcessor {
|
|
15
|
-
const processor = processors.get(name);
|
|
16
|
-
if (!processor) {
|
|
17
|
-
throw new Error(`Processor ${name} not found. Available: ${Array.from(processors.keys()).join(', ')}`);
|
|
18
|
-
}
|
|
19
|
-
return processor;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getRegisteredProcessors(): string[] {
|
|
23
|
-
return Array.from(processors.keys());
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function hasProcessor(name: string): boolean {
|
|
27
|
-
return processors.has(name);
|
|
28
|
-
}
|
package/src/types/index.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
export interface PaymentConfig {
|
|
2
|
-
merchantId: string;
|
|
3
|
-
processor: string;
|
|
4
|
-
testMode: boolean;
|
|
5
|
-
credentials: Record<string, string>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface PaymentInput {
|
|
9
|
-
orderId: string;
|
|
10
|
-
merchantId: string;
|
|
11
|
-
amount: number;
|
|
12
|
-
currency: string;
|
|
13
|
-
processor: string;
|
|
14
|
-
returnUrl?: string;
|
|
15
|
-
cancelUrl?: string;
|
|
16
|
-
metadata?: Record<string, unknown>;
|
|
17
|
-
customer?: {
|
|
18
|
-
id?: string;
|
|
19
|
-
email?: string;
|
|
20
|
-
name?: string;
|
|
21
|
-
};
|
|
22
|
-
paymentMethod?: {
|
|
23
|
-
type: 'card' | 'upi' | 'netbanking' | 'wallet';
|
|
24
|
-
token?: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface PaymentResult {
|
|
29
|
-
success: boolean;
|
|
30
|
-
status: 'authorized' | 'captured' | 'failed' | 'pending' | 'requires_action';
|
|
31
|
-
processorOrderId?: string;
|
|
32
|
-
processorTransactionId?: string;
|
|
33
|
-
redirectUrl?: string;
|
|
34
|
-
errorCode?: string;
|
|
35
|
-
errorMessage?: string;
|
|
36
|
-
metadata?: Record<string, unknown>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface RefundInput {
|
|
40
|
-
orderId: string;
|
|
41
|
-
transactionId: string;
|
|
42
|
-
amount: number;
|
|
43
|
-
reason?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface RefundResult {
|
|
47
|
-
success: boolean;
|
|
48
|
-
refundId?: string;
|
|
49
|
-
status: 'pending' | 'success' | 'failed';
|
|
50
|
-
errorCode?: string;
|
|
51
|
-
errorMessage?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface WebhookDeliveryInput {
|
|
55
|
-
webhookEventId: string;
|
|
56
|
-
merchantId: string;
|
|
57
|
-
webhookUrl: string;
|
|
58
|
-
webhookSecret?: string;
|
|
59
|
-
payload: Record<string, unknown>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface WebhookDeliveryResult {
|
|
63
|
-
success: boolean;
|
|
64
|
-
statusCode?: number;
|
|
65
|
-
attempts: number;
|
|
66
|
-
deliveredAt?: Date;
|
|
67
|
-
errorMessage?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Processor interface that each processor must implement
|
|
71
|
-
export interface PaymentProcessor {
|
|
72
|
-
name: string;
|
|
73
|
-
|
|
74
|
-
createPayment(input: PaymentInput, config: PaymentConfig): Promise<PaymentResult>;
|
|
75
|
-
|
|
76
|
-
capturePayment(
|
|
77
|
-
processorOrderId: string,
|
|
78
|
-
amount: number,
|
|
79
|
-
config: PaymentConfig
|
|
80
|
-
): Promise<PaymentResult>;
|
|
81
|
-
|
|
82
|
-
refundPayment(
|
|
83
|
-
processorTransactionId: string,
|
|
84
|
-
amount: number,
|
|
85
|
-
config: PaymentConfig
|
|
86
|
-
): Promise<RefundResult>;
|
|
87
|
-
|
|
88
|
-
getPaymentStatus(
|
|
89
|
-
processorOrderId: string,
|
|
90
|
-
config: PaymentConfig
|
|
91
|
-
): Promise<PaymentResult>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Processor registry type
|
|
95
|
-
export type ProcessorRegistry = Map<string, PaymentProcessor>;
|
package/src/worker.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// Initialize OpenTelemetry FIRST, before any other imports
|
|
2
|
-
import { initTelemetry } from './lib/observability/otel';
|
|
3
|
-
initTelemetry(process.env.OTEL_SERVICE_NAME || 'loop-processor-core', '0.0.1');
|
|
4
|
-
|
|
5
|
-
import { Worker, NativeConnection } from '@temporalio/worker';
|
|
6
|
-
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
|
7
|
-
import * as activities from './activities';
|
|
8
|
-
import { logger } from './lib/observability/logger';
|
|
9
|
-
|
|
10
|
-
const tracer = trace.getTracer('loop-processor-core');
|
|
11
|
-
|
|
12
|
-
async function run() {
|
|
13
|
-
const connection = await NativeConnection.connect({
|
|
14
|
-
address: process.env.TEMPORAL_ADDRESS || 'localhost:7233'
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const worker = await Worker.create({
|
|
18
|
-
connection,
|
|
19
|
-
namespace: process.env.TEMPORAL_NAMESPACE || 'loop',
|
|
20
|
-
taskQueue: 'payment-queue',
|
|
21
|
-
workflowsPath: new URL('./workflows/index.js', import.meta.url).pathname,
|
|
22
|
-
activities,
|
|
23
|
-
interceptors: {
|
|
24
|
-
activityInbound: [
|
|
25
|
-
(ctx) => ({
|
|
26
|
-
async execute(input, next) {
|
|
27
|
-
const activityType = ctx.info.activityType;
|
|
28
|
-
const workflowId = ctx.info.workflowExecution.workflowId;
|
|
29
|
-
|
|
30
|
-
const span = tracer.startSpan(`activity.${activityType}`, {
|
|
31
|
-
attributes: {
|
|
32
|
-
'temporal.activity.type': activityType,
|
|
33
|
-
'temporal.workflow.id': workflowId,
|
|
34
|
-
'temporal.task_queue': ctx.info.taskQueue
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
logger.info(
|
|
39
|
-
{
|
|
40
|
-
activity: activityType,
|
|
41
|
-
workflowId
|
|
42
|
-
},
|
|
43
|
-
'Activity started'
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const startTime = Date.now();
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const result = await next(input);
|
|
50
|
-
|
|
51
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
52
|
-
|
|
53
|
-
logger.info(
|
|
54
|
-
{
|
|
55
|
-
activity: activityType,
|
|
56
|
-
workflowId,
|
|
57
|
-
duration: Date.now() - startTime
|
|
58
|
-
},
|
|
59
|
-
'Activity completed'
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
return result;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
span.setStatus({
|
|
65
|
-
code: SpanStatusCode.ERROR,
|
|
66
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
67
|
-
});
|
|
68
|
-
span.recordException(error as Error);
|
|
69
|
-
|
|
70
|
-
logger.error(
|
|
71
|
-
{
|
|
72
|
-
activity: activityType,
|
|
73
|
-
workflowId,
|
|
74
|
-
error,
|
|
75
|
-
duration: Date.now() - startTime
|
|
76
|
-
},
|
|
77
|
-
'Activity failed'
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
throw error;
|
|
81
|
-
} finally {
|
|
82
|
-
span.end();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
logger.info(
|
|
91
|
-
{
|
|
92
|
-
taskQueue: 'payment-queue',
|
|
93
|
-
namespace: process.env.TEMPORAL_NAMESPACE || 'loop',
|
|
94
|
-
temporalAddress: process.env.TEMPORAL_ADDRESS || 'localhost:7233'
|
|
95
|
-
},
|
|
96
|
-
'Starting Temporal worker'
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
await worker.run();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
run().catch((err) => {
|
|
103
|
-
logger.error({ error: err }, 'Worker failed');
|
|
104
|
-
process.exit(1);
|
|
105
|
-
});
|
package/src/workflows/index.ts
DELETED
package/src/workflows/payment.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { proxyActivities, sleep, continueAsNew, defineSignal, setHandler } from '@temporalio/workflow';
|
|
2
|
-
import type * as activities from '../activities';
|
|
3
|
-
import type { PaymentInput, PaymentResult } from '../types';
|
|
4
|
-
|
|
5
|
-
const { processPayment, updateOrderStatus, capturePayment } = proxyActivities<typeof activities>({
|
|
6
|
-
startToCloseTimeout: '2 minutes',
|
|
7
|
-
retry: {
|
|
8
|
-
initialInterval: '1s',
|
|
9
|
-
maximumInterval: '1m',
|
|
10
|
-
backoffCoefficient: 2,
|
|
11
|
-
maximumAttempts: 5
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export interface PaymentWorkflowInput {
|
|
16
|
-
orderId: string;
|
|
17
|
-
merchantId: string;
|
|
18
|
-
amount: number;
|
|
19
|
-
currency: string;
|
|
20
|
-
processor: string;
|
|
21
|
-
returnUrl?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Signals for external events
|
|
25
|
-
export const completePaymentSignal = defineSignal<[{ success: boolean; processorTransactionId?: string }]>(
|
|
26
|
-
'completePayment'
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
export const cancelPaymentSignal = defineSignal('cancelPayment');
|
|
30
|
-
|
|
31
|
-
export async function PaymentWorkflow(input: PaymentWorkflowInput): Promise<PaymentResult> {
|
|
32
|
-
let paymentCompleted = false;
|
|
33
|
-
let paymentResult: PaymentResult | null = null;
|
|
34
|
-
let cancelled = false;
|
|
35
|
-
|
|
36
|
-
// Handle external payment completion (3DS, redirect flows)
|
|
37
|
-
setHandler(completePaymentSignal, (result) => {
|
|
38
|
-
paymentCompleted = true;
|
|
39
|
-
paymentResult = {
|
|
40
|
-
success: result.success,
|
|
41
|
-
status: result.success ? 'captured' : 'failed',
|
|
42
|
-
processorTransactionId: result.processorTransactionId
|
|
43
|
-
};
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Handle cancellation
|
|
47
|
-
setHandler(cancelPaymentSignal, () => {
|
|
48
|
-
cancelled = true;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
// Step 1: Process payment
|
|
53
|
-
const result = await processPayment({
|
|
54
|
-
orderId: input.orderId,
|
|
55
|
-
merchantId: input.merchantId,
|
|
56
|
-
amount: input.amount,
|
|
57
|
-
currency: input.currency,
|
|
58
|
-
processor: input.processor,
|
|
59
|
-
returnUrl: input.returnUrl
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// If payment requires redirect (3DS, UPI, etc.)
|
|
63
|
-
if (result.status === 'requires_action') {
|
|
64
|
-
await updateOrderStatus(input.orderId, 'requires_action', result.processorOrderId);
|
|
65
|
-
|
|
66
|
-
// Wait for completion signal (with timeout)
|
|
67
|
-
const timeout = 15 * 60 * 1000; // 15 minutes
|
|
68
|
-
const startTime = Date.now();
|
|
69
|
-
|
|
70
|
-
while (!paymentCompleted && !cancelled && Date.now() - startTime < timeout) {
|
|
71
|
-
await sleep('10 seconds');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (cancelled) {
|
|
75
|
-
await updateOrderStatus(input.orderId, 'cancelled');
|
|
76
|
-
return { success: false, status: 'failed', errorCode: 'cancelled', errorMessage: 'Payment cancelled' };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (!paymentCompleted) {
|
|
80
|
-
await updateOrderStatus(input.orderId, 'failed');
|
|
81
|
-
return { success: false, status: 'failed', errorCode: 'timeout', errorMessage: 'Payment timeout' };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (paymentResult !== null) {
|
|
85
|
-
const finalResult = paymentResult as PaymentResult;
|
|
86
|
-
await updateOrderStatus(
|
|
87
|
-
input.orderId,
|
|
88
|
-
finalResult.status,
|
|
89
|
-
result.processorOrderId,
|
|
90
|
-
finalResult.processorTransactionId
|
|
91
|
-
);
|
|
92
|
-
return finalResult;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Update order with result
|
|
97
|
-
await updateOrderStatus(input.orderId, result.status, result.processorOrderId, result.processorTransactionId);
|
|
98
|
-
|
|
99
|
-
return result;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
102
|
-
await updateOrderStatus(input.orderId, 'failed');
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
success: false,
|
|
106
|
-
status: 'failed',
|
|
107
|
-
errorCode: 'workflow_error',
|
|
108
|
-
errorMessage
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
}
|
package/src/workflows/webhook.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { proxyActivities, sleep } from '@temporalio/workflow';
|
|
2
|
-
import type * as activities from '../activities';
|
|
3
|
-
import type { WebhookDeliveryResult } from '../types';
|
|
4
|
-
|
|
5
|
-
const { deliverWebhook, getMerchantWebhookUrl } = proxyActivities<typeof activities>({
|
|
6
|
-
startToCloseTimeout: '1 minute',
|
|
7
|
-
retry: {
|
|
8
|
-
initialInterval: '1s',
|
|
9
|
-
maximumInterval: '30s',
|
|
10
|
-
backoffCoefficient: 2,
|
|
11
|
-
maximumAttempts: 3
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export interface WebhookDeliveryWorkflowInput {
|
|
16
|
-
webhookEventId: string;
|
|
17
|
-
merchantId: string;
|
|
18
|
-
webhookUrl: string;
|
|
19
|
-
payload: Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const MAX_ATTEMPTS = 5;
|
|
23
|
-
const RETRY_DELAYS = [
|
|
24
|
-
60 * 1000, // 1 minute
|
|
25
|
-
5 * 60 * 1000, // 5 minutes
|
|
26
|
-
30 * 60 * 1000, // 30 minutes
|
|
27
|
-
2 * 60 * 60 * 1000, // 2 hours
|
|
28
|
-
24 * 60 * 60 * 1000 // 24 hours
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
export async function WebhookDeliveryWorkflow(input: WebhookDeliveryWorkflowInput): Promise<WebhookDeliveryResult> {
|
|
32
|
-
// Get merchant's webhook secret
|
|
33
|
-
const { secret } = await getMerchantWebhookUrl(input.merchantId);
|
|
34
|
-
|
|
35
|
-
let attempt = 0;
|
|
36
|
-
let lastResult: WebhookDeliveryResult | null = null;
|
|
37
|
-
|
|
38
|
-
while (attempt < MAX_ATTEMPTS) {
|
|
39
|
-
attempt++;
|
|
40
|
-
|
|
41
|
-
const result = await deliverWebhook(input.webhookEventId, input.webhookUrl, secret || undefined, input.payload);
|
|
42
|
-
|
|
43
|
-
lastResult = result;
|
|
44
|
-
|
|
45
|
-
if (result.success) {
|
|
46
|
-
return result;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// If we've exhausted all attempts, return failure
|
|
50
|
-
if (attempt >= MAX_ATTEMPTS) {
|
|
51
|
-
return {
|
|
52
|
-
success: false,
|
|
53
|
-
attempts: attempt,
|
|
54
|
-
errorMessage: result.errorMessage || 'Max attempts reached'
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Wait before next retry
|
|
59
|
-
const delay = RETRY_DELAYS[attempt - 1] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
|
|
60
|
-
await sleep(delay);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return lastResult || { success: false, attempts: attempt, errorMessage: 'Unknown error' };
|
|
64
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"esModuleInterop": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"outDir": "dist",
|
|
10
|
-
"rootDir": "src",
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"baseUrl": ".",
|
|
14
|
-
"paths": {
|
|
15
|
-
"@/*": ["src/*"]
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|