Skip to content

Commit f9d2d1c

Browse files
fix: enforce status transition atomically to prevent TOCTOU race
Amp-Thread-ID: https://ampcode.com/threads/T-019ca09f-32dc-76ab-b793-4096a1f407fe Co-authored-by: Amp <amp@ampcode.com>
1 parent aea1ebb commit f9d2d1c

3 files changed

Lines changed: 30 additions & 8 deletions

File tree

app/models/transaction.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,26 @@ export async function getTransactionById(
102102
export async function updateTransactionStatus(
103103
id: string,
104104
notes: string,
105-
status: TransactionStatus
105+
status: TransactionStatus,
106+
allowedCurrentStatuses?: TransactionStatus[]
106107
) {
107-
return await db.transaction.update({
108-
where: { id },
109-
data: { notes, status },
110-
})
108+
try {
109+
return await db.transaction.update({
110+
where: allowedCurrentStatuses
111+
? { id, status: { in: allowedCurrentStatuses } }
112+
: { id },
113+
data: { notes, status },
114+
})
115+
} catch (e: unknown) {
116+
// Prisma P2025: record not found (concurrent status change)
117+
if (
118+
e instanceof Error &&
119+
'code' in e &&
120+
(e as { code: string }).code === 'P2025'
121+
) {
122+
return null
123+
}
124+
125+
throw e
126+
}
111127
}

app/routes/__tests__/dashboard.transactions.$transactionId.$action.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ describe('dashboard.transactions.$transactionId.$action action', () => {
129129
expect(updateTransactionStatus).toHaveBeenCalledWith(
130130
'tx-1',
131131
'Catatan verifikasi',
132-
TRANSACTION_STATUS.VERIFIED
132+
TRANSACTION_STATUS.VERIFIED,
133+
[TRANSACTION_STATUS.SUBMITTED, TRANSACTION_STATUS.REJECTED]
133134
)
134135
})
135136
})

app/routes/dashboard.transactions.$transactionId.$action.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,15 @@ export const action: ActionFunction = async ({ request, params }) => {
103103
const transaction = await updateTransactionStatus(
104104
transactionId,
105105
notes,
106-
status
106+
status as TransactionStatus,
107+
status === TRANSACTION_STATUS.REJECTED
108+
? [TRANSACTION_STATUS.SUBMITTED]
109+
: [TRANSACTION_STATUS.SUBMITTED, TRANSACTION_STATUS.REJECTED]
107110
)
108111
if (!transaction) {
109-
throw json({ transaction, notes, status }, { status: 500 })
112+
throw json('Transaksi gagal diperbarui karena status sudah berubah.', {
113+
status: 409,
114+
})
110115
}
111116

112117
if (transaction.status === TRANSACTION_STATUS.VERIFIED) {

0 commit comments

Comments
 (0)