Skip to content

Commit 8b84e1b

Browse files
committed
hi
1 parent b54d911 commit 8b84e1b

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* Payment Intent Pre-Validator Service (T012)
3+
*
4+
* Validates payment intents BEFORE creating orders to prevent:
5+
* - Orders being created with failed/insufficient payments
6+
* - Race conditions between order creation and payment processing
7+
* - Fraudulent checkouts with tampered payment amounts
8+
*
9+
* Requirements:
10+
* - FR-003: Pre-validate payment intent before order creation
11+
* - Must verify: amount matches, status is valid, belongs to correct store
12+
* - Must handle Stripe payment intents (extensible for other providers)
13+
*/
14+
15+
import { db } from '@/lib/db';
16+
import { PaymentError, ValidationError } from '@/lib/errors';
17+
18+
/**
19+
* Payment intent validation result
20+
*/
21+
export interface PaymentIntentValidation {
22+
isValid: boolean;
23+
reason?: string;
24+
paymentIntentId: string;
25+
amount: number;
26+
currency: string;
27+
status: string;
28+
}
29+
30+
/**
31+
* Validate payment intent before order creation
32+
*
33+
* @param paymentIntentId - Stripe payment intent ID
34+
* @param expectedAmount - Expected amount in cents (server-calculated)
35+
* @param storeId - Store ID for multi-tenant isolation
36+
* @returns Validation result with status and reason if invalid
37+
*
38+
* @throws ValidationError if paymentIntentId is missing
39+
* @throws PaymentError if validation fails critically
40+
*/
41+
export async function validatePaymentIntent(
42+
paymentIntentId: string,
43+
expectedAmount: number,
44+
storeId: string
45+
): Promise<PaymentIntentValidation> {
46+
if (!paymentIntentId) {
47+
throw new ValidationError('Payment intent ID is required');
48+
}
49+
50+
try {
51+
// TODO: Integrate with actual Stripe SDK
52+
// For now, check database payment records
53+
// In production, this should call Stripe API:
54+
// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
55+
// const intent = await stripe.paymentIntents.retrieve(paymentIntentId);
56+
57+
// Look up payment intent in database (temporary implementation)
58+
const existingPayment = await db.payment.findFirst({
59+
where: {
60+
paymentIntentId,
61+
order: {
62+
storeId, // Multi-tenant isolation
63+
},
64+
},
65+
select: {
66+
id: true,
67+
amount: true,
68+
currency: true,
69+
status: true,
70+
orderId: true,
71+
},
72+
});
73+
74+
// If payment already exists and is completed, prevent duplicate order creation
75+
if (existingPayment) {
76+
if (existingPayment.status === 'COMPLETED') {
77+
return {
78+
isValid: false,
79+
reason: 'Payment intent already used for a completed order',
80+
paymentIntentId,
81+
amount: Number(existingPayment.amount),
82+
currency: existingPayment.currency,
83+
status: existingPayment.status,
84+
};
85+
}
86+
87+
if (existingPayment.status === 'FAILED' || existingPayment.status === 'CANCELLED') {
88+
return {
89+
isValid: false,
90+
reason: `Payment intent has status: ${existingPayment.status}`,
91+
paymentIntentId,
92+
amount: Number(existingPayment.amount),
93+
currency: existingPayment.currency,
94+
status: existingPayment.status,
95+
};
96+
}
97+
}
98+
99+
// TODO: Replace with actual Stripe API validation
100+
// Mock validation for development
101+
// In production, verify:
102+
// 1. Payment intent exists in Stripe
103+
// 2. Status is 'requires_confirmation' or 'succeeded'
104+
// 3. Amount matches expected amount
105+
// 4. Currency matches store currency
106+
// 5. Payment method is attached
107+
108+
// For now, validate payment intent ID format (Stripe format: pi_*)
109+
if (!paymentIntentId.startsWith('pi_')) {
110+
return {
111+
isValid: false,
112+
reason: 'Invalid payment intent ID format',
113+
paymentIntentId,
114+
amount: expectedAmount,
115+
currency: 'usd',
116+
status: 'invalid',
117+
};
118+
}
119+
120+
// Mock: Assume payment intent is valid if not found in DB yet
121+
// In production, this would be a Stripe API call
122+
return {
123+
isValid: true,
124+
paymentIntentId,
125+
amount: expectedAmount,
126+
currency: 'usd',
127+
status: 'requires_confirmation',
128+
};
129+
} catch (error) {
130+
console.error('Payment intent validation error:', error);
131+
throw new PaymentError(
132+
'Failed to validate payment intent',
133+
{
134+
paymentIntentId,
135+
originalError: error instanceof Error ? error.message : String(error),
136+
}
137+
);
138+
}
139+
}
140+
141+
/**
142+
* Verify payment intent amount matches server-calculated total
143+
*
144+
* Prevents tampering where client submits payment intent with lower amount
145+
* than actual order total.
146+
*
147+
* @param paymentIntentId - Stripe payment intent ID
148+
* @param expectedAmount - Server-calculated order total (in cents)
149+
* @returns True if amounts match within 1 cent tolerance
150+
*/
151+
export async function verifyPaymentIntentAmount(
152+
paymentIntentId: string,
153+
expectedAmount: number
154+
): Promise<boolean> {
155+
// TODO: Implement Stripe API call to retrieve payment intent amount
156+
// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
157+
// const intent = await stripe.paymentIntents.retrieve(paymentIntentId);
158+
// const actualAmount = intent.amount;
159+
160+
// For now, return true (mock implementation)
161+
// In production, compare with 1 cent tolerance to account for rounding
162+
// return Math.abs(actualAmount - expectedAmount) <= 1;
163+
164+
return true;
165+
}
166+
167+
/**
168+
* Check if payment intent belongs to the specified store
169+
*
170+
* Multi-tenant isolation: Prevent using payment intents from other stores
171+
*
172+
* @param paymentIntentId - Stripe payment intent ID
173+
* @param storeId - Expected store ID
174+
* @returns True if payment intent belongs to store
175+
*/
176+
export async function verifyPaymentIntentStore(
177+
paymentIntentId: string,
178+
storeId: string
179+
): Promise<boolean> {
180+
// TODO: Implement Stripe metadata check
181+
// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
182+
// const intent = await stripe.paymentIntents.retrieve(paymentIntentId);
183+
// return intent.metadata.storeId === storeId;
184+
185+
// For now, check database payment records
186+
const payment = await db.payment.findFirst({
187+
where: {
188+
paymentIntentId,
189+
order: {
190+
storeId,
191+
},
192+
},
193+
select: { id: true },
194+
});
195+
196+
return !!payment;
197+
}
198+
199+
/**
200+
* Get valid payment statuses for checkout
201+
*
202+
* @returns Array of payment statuses that allow order creation
203+
*/
204+
export function getValidCheckoutStatuses(): string[] {
205+
return [
206+
'requires_confirmation',
207+
'requires_action',
208+
'processing',
209+
'succeeded',
210+
];
211+
}

0 commit comments

Comments
 (0)