Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .specs/kiloclaw-datamodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,6 @@ not yet enforced in the current codebase:
across all services that mutate subscription records. Some
subscription-creation paths may already write change-log entries;
complete cross-service coverage remains the intended invariant.
4. Fresh Provision Admission SHOULD be implemented in the Registry-backed
Worker admission flow before the existing web advisory lock is removed.
(Currently, web requests use transitional PostgreSQL advisory-lock
coordination that is being replaced because it is unsafe through
transaction-pooled production connections.)

## Changelog

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ describe('ReferralRewardStatusCard', () => {
application: {
appliedAt: '2026-04-10T00:05:00.000Z',
subscriptionId: '11111111-1111-4111-8111-111111111111',
previousRenewalBoundary: '2026-05-01T00:00:00.000Z',
newRenewalBoundary: '2026-06-01T00:00:00.000Z',
previousRenewalBoundary: '2026-05-01T12:00:00.000Z',
newRenewalBoundary: '2026-06-01T12:00:00.000Z',
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ describe('ReferralRewardsSummary', () => {
role: 'referrer',
appliedAt: '2026-04-10T00:05:00.000Z',
monthsGranted: 1,
previousRenewalBoundary: '2026-05-01T00:00:00.000Z',
newRenewalBoundary: '2026-06-01T00:00:00.000Z',
previousRenewalBoundary: '2026-05-01T12:00:00.000Z',
newRenewalBoundary: '2026-06-01T12:00:00.000Z',
},
{
role: 'referee',
appliedAt: '2026-04-11T00:05:00.000Z',
monthsGranted: 1,
previousRenewalBoundary: '2026-06-01T00:00:00.000Z',
newRenewalBoundary: '2026-07-01T00:00:00.000Z',
previousRenewalBoundary: '2026-06-01T12:00:00.000Z',
newRenewalBoundary: '2026-07-01T12:00:00.000Z',
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ function referralRow(params: {
id: `${params.referralId}-application`,
beneficiaryUserId: 'referrer-1',
subscriptionId: '55555555-5555-4555-8555-555555555555',
previousRenewalBoundary: '2026-05-01T00:00:00.000Z',
newRenewalBoundary: '2026-06-01T00:00:00.000Z',
previousRenewalBoundary: '2026-05-01T12:00:00.000Z',
newRenewalBoundary: '2026-06-01T12:00:00.000Z',
appliedAt: '2026-04-10T00:05:00.000Z',
},
]
Expand Down
18 changes: 9 additions & 9 deletions apps/web/src/lib/impact/kiloclaw-referrals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ async function insertActivePersonalSubscription(
plan: 'standard',
status: 'active',
current_period_start: '2026-04-01T00:00:00.000Z',
current_period_end: '2026-05-01T00:00:00.000Z',
credit_renewal_at: '2026-05-01T00:00:00.000Z',
current_period_end: '2026-05-01T12:00:00.000Z',
credit_renewal_at: '2026-05-01T12:00:00.000Z',
cancel_at_period_end: false,
...overrides,
})
Expand Down Expand Up @@ -636,7 +636,7 @@ describe('kiloclaw referrals', () => {
expect(applications).toHaveLength(2);
expect(
applications.map(application => String(application.new_renewal_boundary)).sort()
).toEqual(['2026-06-01 00:00:00+00', '2026-06-01 00:00:00+00']);
).toEqual(['2026-06-01 12:00:00+00', '2026-06-01 12:00:00+00']);

const subscriptions = await db
.select({
Expand All @@ -650,13 +650,13 @@ describe('kiloclaw referrals', () => {
expect.arrayContaining([
expect.objectContaining({
userId: referrer.id,
currentPeriodEnd: '2026-06-01 00:00:00+00',
creditRenewalAt: '2026-06-01 00:00:00+00',
currentPeriodEnd: '2026-06-01 12:00:00+00',
creditRenewalAt: '2026-06-01 12:00:00+00',
}),
expect.objectContaining({
userId: referee.id,
currentPeriodEnd: '2026-06-01 00:00:00+00',
creditRenewalAt: '2026-06-01 00:00:00+00',
currentPeriodEnd: '2026-06-01 12:00:00+00',
creditRenewalAt: '2026-06-01 12:00:00+00',
}),
])
);
Expand Down Expand Up @@ -1573,7 +1573,7 @@ describe('kiloclaw referrals', () => {
.select()
.from(kiloclaw_subscriptions)
.where(eq(kiloclaw_subscriptions.user_id, referee.id));
expect(subscription.current_period_end).toBe('2026-05-01 00:00:00+00');
expect(subscription.current_period_end).toBe('2026-05-01 12:00:00+00');

const refereeRewards = await db
.select({
Expand Down Expand Up @@ -1646,7 +1646,7 @@ describe('kiloclaw referrals', () => {
'sub_referee_123',
expect.objectContaining({
proration_behavior: 'none',
trial_end: Math.floor(new Date('2026-06-01T00:00:00.000Z').getTime() / 1000),
trial_end: Math.floor(new Date('2026-06-01T12:00:00.000Z').getTime() / 1000),
}),
expect.objectContaining({
idempotencyKey: expect.stringContaining('stripe-apply'),
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/lib/kilo-pass/apple-store-notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function transaction(
bundleId: 'com.kilocode.kiloapp',
productId: 'kilopass.tier19.monthly.v1',
purchaseDate: 1_777_626_000_000,
expiresDate: 1_780_218_000_000,
expiresDate: Date.parse('2030-06-01T00:00:00.000Z'),
environment: 'Sandbox',
rawPayload: { test: true },
...overrides,
Expand Down Expand Up @@ -1152,8 +1152,8 @@ describe('processAppStoreKiloPassNotification', () => {
transactionId: tx1,
productId: 'kilopass.tier19.monthly.v1',
appAccountToken: user.app_store_account_token,
purchaseDate: Date.parse('2026-05-01T00:00:00.000Z'),
expiresDate: Date.parse('2026-05-31T00:00:00.000Z'),
purchaseDate: Date.parse('2026-06-01T00:00:00.000Z'),
expiresDate: Date.parse('2026-07-01T00:00:00.000Z'),
currency: 'USD',
price: 19000,
}),
Expand All @@ -1173,8 +1173,8 @@ describe('processAppStoreKiloPassNotification', () => {
transactionId: tx2,
productId: 'kilopass.tier49.monthly.v1',
appAccountToken: user.app_store_account_token,
purchaseDate: Date.parse('2026-05-16T00:00:00.000Z'),
expiresDate: Date.parse('2026-06-16T00:00:00.000Z'),
purchaseDate: Date.parse('2026-06-16T00:00:00.000Z'),
expiresDate: Date.parse('2026-07-16T00:00:00.000Z'),
currency: 'USD',
price: 49000,
}),
Expand All @@ -1188,7 +1188,7 @@ describe('processAppStoreKiloPassNotification', () => {
const issuance = await db.query.kilo_pass_issuances.findFirst({
where: and(
eq(kilo_pass_issuances.kilo_pass_subscription_id, subscription?.id ?? ''),
eq(kilo_pass_issuances.issue_month, '2026-05-01')
eq(kilo_pass_issuances.issue_month, '2026-06-01')
),
});
expect(issuance).toBeDefined();
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/lib/kiloclaw/kiloclaw-internal-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,21 @@ export class KiloClawInternalClient {
);
}

async repairProvisionReservation(
userId: string,
instanceId: string,
orgId?: string
): Promise<{ ok: true }> {
return this.request(
'/api/platform/provision/repair-reservation',
{
method: 'POST',
body: JSON.stringify({ userId, instanceId, orgId }),
},
{ userId }
);
}

async start(
userId: string,
instanceId?: string,
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/lib/kiloclaw/provision-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TRPCError } from '@trpc/server';
import { UpstreamApiError } from '@/lib/trpc/init';
import { KiloClawApiError } from './kiloclaw-internal-client';

type ProvisionErrorPayload = { message?: string; code?: string };

type ProvisionErrorPayloadReader = (err: KiloClawApiError) => ProvisionErrorPayload;

export function handleProvisionError(err: unknown, getPayload: ProvisionErrorPayloadReader): never {
if (err instanceof KiloClawApiError) {
const { message, code } = getPayload(err);
if (
(err.statusCode === 409 || err.statusCode === 503) &&
(code === 'provision_in_progress' ||
code === 'provision_completion_pending' ||
code === 'instance_already_active' ||
code === 'instance_destroyed')
) {
throw new TRPCError({
code: 'CONFLICT',
message:
message ??
'An instance is already being created. Wait for setup to finish, then try again.',
cause: new UpstreamApiError(code),
});
}
if (err.statusCode === 404 && code === 'instance_not_found') {
throw new TRPCError({
code: 'NOT_FOUND',
message: message ?? 'No active KiloClaw instance found',
cause: new UpstreamApiError(code),
});
}
}
throw err;
}
Loading
Loading