@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.
@@ -0,0 +1,224 @@
1
+ import { eq, and } from 'drizzle-orm';
2
+ import { getDb, orders, transactions, processorConfigs, webhookEvents, merchants } from '../lib/db';
3
+ import { decrypt } from '../lib/crypto';
4
+ import { getProcessor } from '../lib/registry';
5
+ import type { PaymentInput, PaymentResult, PaymentConfig, RefundResult, WebhookDeliveryResult } from '../types';
6
+ import crypto from 'crypto';
7
+
8
+ // Get processor config for a merchant
9
+ export async function getProcessorConfig(
10
+ merchantId: string,
11
+ processor: string
12
+ ): Promise<PaymentConfig | null> {
13
+ const db = getDb();
14
+
15
+ const config = await db
16
+ .select()
17
+ .from(processorConfigs)
18
+ .where(and(eq(processorConfigs.merchantId, merchantId), eq(processorConfigs.processor, processor)))
19
+ .limit(1);
20
+
21
+ if (config.length === 0) return null;
22
+
23
+ const credentials = JSON.parse(decrypt(config[0].credentialsEncrypted));
24
+
25
+ return {
26
+ merchantId,
27
+ processor,
28
+ testMode: config[0].testMode,
29
+ credentials
30
+ };
31
+ }
32
+
33
+ // Update order status
34
+ export async function updateOrderStatus(
35
+ orderId: string,
36
+ status: string,
37
+ processorOrderId?: string,
38
+ processorTransactionId?: string
39
+ ): Promise<void> {
40
+ const db = getDb();
41
+
42
+ await db
43
+ .update(orders)
44
+ .set({
45
+ status,
46
+ processorOrderId: processorOrderId || undefined,
47
+ updatedAt: new Date()
48
+ })
49
+ .where(eq(orders.id, orderId));
50
+
51
+ // Create transaction record if we have a transaction id
52
+ if (processorTransactionId) {
53
+ const order = await db.select().from(orders).where(eq(orders.id, orderId)).limit(1);
54
+
55
+ if (order.length > 0) {
56
+ await db.insert(transactions).values({
57
+ orderId,
58
+ type: status === 'captured' ? 'capture' : status === 'authorized' ? 'authorization' : 'authorization',
59
+ amount: order[0].amount,
60
+ status: status === 'failed' ? 'failed' : 'success',
61
+ processorTransactionId
62
+ });
63
+ }
64
+ }
65
+ }
66
+
67
+ // Process payment using the appropriate processor
68
+ export async function processPayment(input: PaymentInput): Promise<PaymentResult> {
69
+ const config = await getProcessorConfig(input.merchantId, input.processor);
70
+
71
+ if (!config) {
72
+ return {
73
+ success: false,
74
+ status: 'failed',
75
+ errorCode: 'no_processor_config',
76
+ errorMessage: `Processor ${input.processor} not configured for merchant`
77
+ };
78
+ }
79
+
80
+ const processor = getProcessor(input.processor);
81
+ return processor.createPayment(input, config);
82
+ }
83
+
84
+ // Capture a payment
85
+ export async function capturePayment(
86
+ processorOrderId: string,
87
+ amount: number,
88
+ merchantId: string,
89
+ processor: string
90
+ ): Promise<PaymentResult> {
91
+ const config = await getProcessorConfig(merchantId, processor);
92
+
93
+ if (!config) {
94
+ return {
95
+ success: false,
96
+ status: 'failed',
97
+ errorCode: 'no_processor_config',
98
+ errorMessage: `Processor ${processor} not configured`
99
+ };
100
+ }
101
+
102
+ const processorInstance = getProcessor(processor);
103
+ return processorInstance.capturePayment(processorOrderId, amount, config);
104
+ }
105
+
106
+ // Refund a payment
107
+ export async function refundPayment(
108
+ processorTransactionId: string,
109
+ amount: number,
110
+ merchantId: string,
111
+ processor: string
112
+ ): Promise<RefundResult> {
113
+ const config = await getProcessorConfig(merchantId, processor);
114
+
115
+ if (!config) {
116
+ return {
117
+ success: false,
118
+ status: 'failed',
119
+ errorCode: 'no_processor_config',
120
+ errorMessage: `Processor ${processor} not configured`
121
+ };
122
+ }
123
+
124
+ const processorInstance = getProcessor(processor);
125
+ return processorInstance.refundPayment(processorTransactionId, amount, config);
126
+ }
127
+
128
+ // Deliver webhook to merchant
129
+ export async function deliverWebhook(
130
+ webhookEventId: string,
131
+ webhookUrl: string,
132
+ webhookSecret: string | undefined,
133
+ payload: Record<string, unknown>
134
+ ): Promise<WebhookDeliveryResult> {
135
+ const db = getDb();
136
+
137
+ // Get current attempt count
138
+ const event = await db.select().from(webhookEvents).where(eq(webhookEvents.id, webhookEventId)).limit(1);
139
+
140
+ const attempts = (event[0]?.attempts || 0) + 1;
141
+
142
+ try {
143
+ const body = JSON.stringify(payload);
144
+ const headers: Record<string, string> = {
145
+ 'Content-Type': 'application/json',
146
+ 'X-Loop-Event-Id': webhookEventId,
147
+ 'X-Loop-Timestamp': String(Date.now())
148
+ };
149
+
150
+ // Sign the webhook if secret is provided
151
+ if (webhookSecret) {
152
+ const timestamp = headers['X-Loop-Timestamp'];
153
+ const signaturePayload = `${timestamp}.${body}`;
154
+ const signature = crypto.createHmac('sha256', webhookSecret).update(signaturePayload).digest('hex');
155
+ headers['X-Loop-Signature'] = `v1=${signature}`;
156
+ }
157
+
158
+ const response = await fetch(webhookUrl, {
159
+ method: 'POST',
160
+ headers,
161
+ body,
162
+ signal: AbortSignal.timeout(30000) // 30 second timeout
163
+ });
164
+
165
+ const success = response.ok;
166
+
167
+ await db
168
+ .update(webhookEvents)
169
+ .set({
170
+ status: success ? 'delivered' : 'pending',
171
+ attempts,
172
+ lastAttemptAt: new Date(),
173
+ deliveredAt: success ? new Date() : undefined,
174
+ nextRetryAt: success ? undefined : new Date(Date.now() + getRetryDelay(attempts))
175
+ })
176
+ .where(eq(webhookEvents.id, webhookEventId));
177
+
178
+ return {
179
+ success,
180
+ statusCode: response.status,
181
+ attempts,
182
+ deliveredAt: success ? new Date() : undefined,
183
+ errorMessage: success ? undefined : `HTTP ${response.status}`
184
+ };
185
+ } catch (error) {
186
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
187
+
188
+ await db
189
+ .update(webhookEvents)
190
+ .set({
191
+ status: attempts >= 5 ? 'failed' : 'pending',
192
+ attempts,
193
+ lastAttemptAt: new Date(),
194
+ nextRetryAt: attempts >= 5 ? undefined : new Date(Date.now() + getRetryDelay(attempts))
195
+ })
196
+ .where(eq(webhookEvents.id, webhookEventId));
197
+
198
+ return {
199
+ success: false,
200
+ attempts,
201
+ errorMessage
202
+ };
203
+ }
204
+ }
205
+
206
+ // Get merchant webhook URL
207
+ export async function getMerchantWebhookUrl(merchantId: string): Promise<{ url: string | null; secret: string | null }> {
208
+ const db = getDb();
209
+
210
+ const merchant = await db.select().from(merchants).where(eq(merchants.id, merchantId)).limit(1);
211
+
212
+ return {
213
+ url: merchant[0]?.webhookUrl || null,
214
+ secret: merchant[0]?.webhookSecret || null
215
+ };
216
+ }
217
+
218
+ // Exponential backoff for retries
219
+ function getRetryDelay(attempt: number): number {
220
+ const baseDelay = 60 * 1000; // 1 minute
221
+ const maxDelay = 24 * 60 * 60 * 1000; // 24 hours
222
+ const delay = baseDelay * Math.pow(2, attempt - 1);
223
+ return Math.min(delay, maxDelay);
224
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ // Core types
2
+ export type {
3
+ PaymentConfig,
4
+ PaymentInput,
5
+ PaymentResult,
6
+ RefundInput,
7
+ RefundResult,
8
+ WebhookDeliveryInput,
9
+ WebhookDeliveryResult,
10
+ PaymentProcessor,
11
+ ProcessorRegistry
12
+ } from './types';
13
+
14
+ // Processor registry
15
+ export { registerProcessor, getProcessor, getRegisteredProcessors, hasProcessor } from './lib/registry';
16
+
17
+ // Activities (for worker registration)
18
+ export * as activities from './activities';
19
+
20
+ // Workflows
21
+ export { PaymentWorkflow, WebhookDeliveryWorkflow } from './workflows';
22
+ export type { PaymentWorkflowInput, WebhookDeliveryWorkflowInput } from './workflows';
@@ -0,0 +1,24 @@
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 ADDED
@@ -0,0 +1,91 @@
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>;
@@ -0,0 +1,2 @@
1
+ export { initTelemetry } from './otel';
2
+ export { logger, createActivityLogger, createWorkflowLogger } from './logger';
@@ -0,0 +1,44 @@
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
+ }
@@ -0,0 +1,53 @@
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
+ }
@@ -0,0 +1,28 @@
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
+ }
@@ -0,0 +1,95 @@
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 ADDED
@@ -0,0 +1,105 @@
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
+ });
@@ -0,0 +1,5 @@
1
+ export { PaymentWorkflow, completePaymentSignal, cancelPaymentSignal } from './payment';
2
+ export type { PaymentWorkflowInput } from './payment';
3
+
4
+ export { WebhookDeliveryWorkflow } from './webhook';
5
+ export type { WebhookDeliveryWorkflowInput } from './webhook';