diff --git a/server/accounts/db.ts b/server/accounts/db.ts index c1397e1a..f78cc215 100644 --- a/server/accounts/db.ts +++ b/server/accounts/db.ts @@ -43,7 +43,7 @@ export async function UpdateBalance( if (amount <= 0) return { success: false, message: 'invalid_amount' }; - using conn = await GetConnection(); + await using conn = await GetConnection(); const balance = await conn.scalar(getBalance, [accountId]); if (balance === null) @@ -53,14 +53,21 @@ export async function UpdateBalance( }; const addAction = action === 'add'; + + await conn.beginTransaction(); + const success = addAction ? await conn.update(addBalance, [amount, accountId]) : await conn.update(overdraw ? removeBalance : safeRemoveBalance, [amount, accountId, amount]); - if (!success) + + if (!success) { + await conn.rollback(); + return { success: false, message: 'insufficient_balance', }; + } !message && (message = locales(action === 'add' ? 'deposit' : 'withdraw')); @@ -76,17 +83,59 @@ export async function UpdateBalance( addAction ? balance + amount : null, ])) === 1; - if (!didUpdate) + if (!didUpdate) { + await conn.rollback(); + return { success: false, message: 'something_went_wrong', }; + } + + await conn.commit(); emit('ox:updatedBalance', { accountId, amount, action }); return { success: true }; } +/** Moves funds between two accounts on an existing transaction, without committing. */ +async function ApplyTransfer( + conn: Connection, + fromId: number, + toId: number, + amount: number, + overdraw: boolean, + fromBalance: number, + toBalance: number, + message?: string, + note?: string, + actorId?: number, +) { + const query = overdraw ? removeBalance : safeRemoveBalance; + const values = [amount, fromId]; + + if (!overdraw) values.push(amount); + + const removedBalance = await conn.update(query, values); + const addedBalance = removedBalance && (await conn.update(addBalance, [amount, toId])); + + if (!addedBalance) return false; + + await conn.execute(addTransaction, [ + actorId, + fromId, + toId, + amount, + message ?? locales('transfer'), + note, + fromBalance - amount, + toBalance + amount, + ]); + + return true; +} + export async function PerformTransaction( fromId: number, toId: number, @@ -102,7 +151,7 @@ export async function PerformTransaction( if (amount <= 0) return { success: false, message: 'invalid_amount' }; - using conn = await GetConnection(); + await using conn = await GetConnection(); const fromBalance = await conn.scalar(getBalance, [fromId]); const toBalance = await conn.scalar(getBalance, [toId]); @@ -112,25 +161,8 @@ export async function PerformTransaction( await conn.beginTransaction(); try { - const query = overdraw ? removeBalance : safeRemoveBalance; - const values = [amount, fromId]; - - if (!overdraw) values.push(amount); - - const removedBalance = await conn.update(query, values); - const addedBalance = removedBalance && (await conn.update(addBalance, [amount, toId])); - - if (addedBalance) { - await conn.execute(addTransaction, [ - actorId, - fromId, - toId, - amount, - message ?? locales('transfer'), - note, - fromBalance - amount, - toBalance + amount, - ]); + if (await ApplyTransfer(conn, fromId, toId, amount, overdraw, fromBalance, toBalance, message, note, actorId)) { + await conn.commit(); emit('ox:transferredMoney', { fromId, toId, amount }); @@ -141,7 +173,7 @@ export async function PerformTransaction( console.log(e); } - conn.rollback(); + await conn.rollback(); return { success: false, message: 'something_went_wrong' }; } @@ -221,7 +253,7 @@ export async function DepositMoney( if (amount > money) return { success: false, message: 'insufficient_funds' }; - using conn = await GetConnection(); + await using conn = await GetConnection(); const balance = await conn.scalar(getBalance, [accountId]); if (balance === null) return { success: false, message: 'no_balance' }; @@ -235,7 +267,7 @@ export async function DepositMoney( const affectedRows = await conn.update(addBalance, [amount, accountId]); if (!affectedRows || !exports.ox_inventory.RemoveItem(playerId, 'money', amount)) { - conn.rollback(); + await conn.rollback(); return { success: false, message: 'something_went_wrong', @@ -253,6 +285,8 @@ export async function DepositMoney( balance + amount, ]); + await conn.commit(); + emit('ox:depositedMoney', { playerId, accountId, amount }); return { @@ -277,7 +311,7 @@ export async function WithdrawMoney( if (!player?.charId) return { success: false, message: 'no_charId' }; - using conn = await GetConnection(); + await using conn = await GetConnection(); const role = await conn.scalar(selectAccountRole, [accountId, player.charId]); if (!(await CanPerformAction(player, accountId, role, 'withdraw'))) return { success: false, message: 'no_access' }; @@ -291,7 +325,7 @@ export async function WithdrawMoney( const affectedRows = await conn.update(safeRemoveBalance, [amount, accountId, amount]); if (!affectedRows || !exports.ox_inventory.AddItem(playerId, 'money', amount)) { - conn.rollback(); + await conn.rollback(); return { success: false, message: 'something_went_wrong', @@ -309,6 +343,8 @@ export async function WithdrawMoney( null, ]); + await conn.commit(); + emit('ox:withdrewMoney', { playerId, accountId, amount }); return { success: true }; @@ -359,49 +395,59 @@ export async function UpdateInvoice( if (!hasPermission) return { success: false, message: 'no_permission' }; - const updateReceiver = await UpdateBalance( - invoice.toAccount, - invoice.amount, - 'remove', - false, - locales('invoice_payment'), - undefined, - charId, - ); + await using conn = await GetConnection(); - if (!updateReceiver.success) return { success: false, message: 'no_balance' }; + const fromBalance = await conn.scalar(getBalance, [invoice.toAccount]); + const toBalance = await conn.scalar(getBalance, [invoice.fromAccount]); - const updateSender = await UpdateBalance( - invoice.fromAccount, - invoice.amount, - 'add', - false, - locales('invoice_payment'), - undefined, - charId, - ); + if (fromBalance === null || toBalance === null) return { success: false, message: 'no_balance' }; - if (!updateSender.success) return { success: false, message: 'no_balance' }; + await conn.beginTransaction(); - const invoiceUpdated = await db.update('UPDATE `accounts_invoices` SET `payerId` = ?, `paidAt` = ? WHERE `id` = ?', [ - player.charId, - new Date(), - invoiceId, - ]); + try { + const transferred = await ApplyTransfer( + conn, + invoice.toAccount, + invoice.fromAccount, + invoice.amount, + false, + fromBalance, + toBalance, + locales('invoice_payment'), + undefined, + charId, + ); + + if (!transferred) { + await conn.rollback(); + return { success: false, message: 'no_balance' }; + } - if (!invoiceUpdated) - return { - success: false, - message: 'invoice_not_updated', - }; + const invoiceUpdated = await conn.update( + 'UPDATE `accounts_invoices` SET `payerId` = ?, `paidAt` = ? WHERE `id` = ?', + [player.charId, new Date(), invoiceId], + ); + + if (!invoiceUpdated) { + await conn.rollback(); + return { success: false, message: 'invoice_not_updated' }; + } + + await conn.commit(); + } catch (e) { + console.error(`Failed to pay invoice ${invoiceId}`); + console.log(e); + + await conn.rollback(); + return { success: false, message: 'something_went_wrong' }; + } invoice.payerId = charId; + emit('ox:transferredMoney', { fromId: invoice.toAccount, toId: invoice.fromAccount, amount: invoice.amount }); emit('ox:invoicePaid', invoice); - return { - success: true, - }; + return { success: true }; } export async function CreateInvoice({ diff --git a/server/db/index.ts b/server/db/index.ts index b8c80c0c..2adb39d5 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -4,6 +4,7 @@ import type { Dict } from 'types'; import type { PoolConnection, QueryOptions } from 'mariadb'; (Symbol as any).dispose ??= Symbol('Symbol.dispose'); +(Symbol as any).asyncDispose ??= Symbol('Symbol.asyncDispose'); export interface MySqlRow | undefined> { [column: string]: T; @@ -72,8 +73,13 @@ export class Connection { return this.connection.commit(); } + async [Symbol.asyncDispose]() { + if (this.transaction) await this.rollback(); + await this.connection.release(); + } + [Symbol.dispose]() { - if (this.transaction) this.commit(); + if (this.transaction) this.rollback(); this.connection.release(); } } diff --git a/server/groups/db.ts b/server/groups/db.ts index 20d491f3..2d87c32f 100644 --- a/server/groups/db.ts +++ b/server/groups/db.ts @@ -20,7 +20,7 @@ export function SelectGroups() { } export async function InsertGroup({ name, label, type, colour, hasAccount, grades, accountRoles }: DbGroup) { - using conn = await GetConnection(); + await using conn = await GetConnection(); await conn.beginTransaction(); const insertedGroup = await conn.update( @@ -35,6 +35,8 @@ export async function InsertGroup({ name, label, type, colour, hasAccount, grade grades.map((gradeLabel, index) => [name, index + 1, gradeLabel, accountRoles[index + 1]]), )) as UpsertResult[]; + await conn.commit(); + return insertedGrades.reduce((acc, curr) => acc + curr.affectedRows, 0) > 0; }