Skip to content

Commit 33eb8ec

Browse files
committed
fix(stripe): avoid relinking deleted fraud warning owners
1 parent e2307a5 commit 33eb8ec

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

apps/web/src/lib/stripe/early-fraud-warning.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
StripeEarlyFraudWarningOwnerClassification,
1212
type StripeEarlyFraudWarningOwnerClassification as OwnerClassification,
1313
} from '@kilocode/db/schema-types';
14-
import { and, eq, isNull } from 'drizzle-orm';
14+
import { and, eq, isNull, like, not, or } from 'drizzle-orm';
1515
import type Stripe from 'stripe';
1616

1717
import { db } from '@/lib/drizzle';
@@ -74,7 +74,15 @@ async function resolveOwner(customerId: string | null): Promise<OwnerResolution>
7474
db
7575
.select({ id: kilocode_users.id })
7676
.from(kilocode_users)
77-
.where(eq(kilocode_users.stripe_customer_id, customerId))
77+
.where(
78+
and(
79+
eq(kilocode_users.stripe_customer_id, customerId),
80+
or(
81+
isNull(kilocode_users.blocked_reason),
82+
not(like(kilocode_users.blocked_reason, 'soft-deleted at %'))
83+
)
84+
)
85+
)
7886
.limit(2),
7987
db
8088
.select({ id: organizations.id })

apps/web/src/lib/stripe/index.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
} from '@kilocode/db/schema';
8282
import { db, auto_deleted_at } from '@/lib/drizzle';
8383
import { insertTestUser } from '@/tests/helpers/user.helper';
84+
import { softDeleteUser } from '@/lib/user';
8485
import { createTestPaymentMethod } from '@/tests/helpers/payment-method.helper';
8586
import { eq, and, count } from 'drizzle-orm';
8687
import type Stripe from 'stripe';
@@ -882,6 +883,43 @@ describe('processStripePaymentEventHook', () => {
882883
retrieveSpy.mockRestore();
883884
});
884885

886+
test('radar.early_fraud_warning.created does not link a new case to a soft-deleted user', async () => {
887+
await cleanupDbForTest();
888+
testUser = await insertTestUser();
889+
await softDeleteUser(testUser.id);
890+
const { client } = await import('@/lib/stripe-client');
891+
const retrieveSpy = jest.spyOn(client.charges, 'retrieve').mockResolvedValue(
892+
sampleStripeChargeResponse(
893+
sampleStripeCharge({
894+
id: 'ch_deleted_customer',
895+
customer: testUser.stripe_customer_id,
896+
})
897+
)
898+
);
899+
900+
await processStripePaymentEventHook(
901+
sampleEarlyFraudWarningEvent({
902+
eventId: 'evt_deleted_customer',
903+
warningId: 'issfr_deleted_customer',
904+
charge: 'ch_deleted_customer',
905+
})
906+
);
907+
908+
const [fraudCase] = await db.select().from(stripe_early_fraud_warning_cases);
909+
expect(fraudCase).toEqual(
910+
expect.objectContaining({
911+
stripe_customer_id: testUser.stripe_customer_id,
912+
owner_classification: 'unmatched',
913+
kilo_user_id: null,
914+
status: 'review_required',
915+
reason: 'No canonical customer owner matched; manual review required',
916+
})
917+
);
918+
expect(await db.select().from(stripe_early_fraud_warning_actions)).toHaveLength(0);
919+
920+
retrieveSpy.mockRestore();
921+
});
922+
885923
test('radar.early_fraud_warning.created deduplicates repeated delivery without creating actions', async () => {
886924
await cleanupDbForTest();
887925
testUser = await insertTestUser();

0 commit comments

Comments
 (0)