diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 1eee1b9a4..eed4278d2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0035-admin_pending_creations_decimal', + DB_VERSION: '0036-unique_previous_in_transactions', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 6842f09ca..ca6bf0fe7 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1053,6 +1053,53 @@ describe('AdminResolver', () => { expect(transaction[0].typeId).toEqual(1) }) }) + + describe('confirm two creations one after the other quickly', () => { + let c1: AdminPendingCreation | void + let c2: AdminPendingCreation | void + + beforeAll(async () => { + const now = new Date() + c1 = await creationFactory(testEnv, { + email: 'bibi@bloxberg.de', + amount: 50, + memo: 'Herzlich Willkommen bei Gradido liebe Bibi!', + creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(), + }) + c2 = await creationFactory(testEnv, { + email: 'bibi@bloxberg.de', + amount: 50, + memo: 'Herzlich Willkommen bei Gradido liebe Bibi!', + creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(), + }) + }) + + // In the futrue this should not throw anymore + it('throws an error for the second confirmation', async () => { + const r1 = mutate({ + mutation: confirmPendingCreation, + variables: { + id: c1 ? c1.id : -1, + }, + }) + const r2 = mutate({ + mutation: confirmPendingCreation, + variables: { + id: c2 ? c2.id : -1, + }, + }) + await expect(r1).resolves.toEqual( + expect.objectContaining({ + data: { confirmPendingCreation: true }, + }), + ) + await expect(r2).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Unable to confirm creation.')], + }), + ) + }) + }) }) }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 78cbf3fc8..8da92a61c 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -361,7 +361,9 @@ export class AdminResolver { transaction.balanceDate = receivedCallDate transaction.decay = decay ? decay.decay : new Decimal(0) transaction.decayStart = decay ? decay.start : null - await transaction.save() + await transaction.save().catch(() => { + throw new Error('Unable to confirm creation.') + }) await AdminPendingCreation.delete(pendingCreation) diff --git a/database/entity/0036-unique_previous_in_transactions/Transaction.ts b/database/entity/0036-unique_previous_in_transactions/Transaction.ts new file mode 100644 index 000000000..99202eee4 --- /dev/null +++ b/database/entity/0036-unique_previous_in_transactions/Transaction.ts @@ -0,0 +1,94 @@ +import Decimal from 'decimal.js-light' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null }) + previous: number | null + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + balance: Decimal + + @Column({ + name: 'balance_date', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + decay: Decimal + + @Column({ + name: 'decay_start', + type: 'datetime', + nullable: true, + default: null, + }) + decayStart: Date | null + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null }) + creationDate: Date | null + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedTransactionId?: number | null + + @Column({ + name: 'transaction_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + transactionLinkId?: number | null +} diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index 3515202d0..5365b0f70 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0032-add-transaction-link-to-transaction/Transaction' +export { Transaction } from './0036-unique_previous_in_transactions/Transaction' diff --git a/database/migrations/0036-unique_previous_in_transactions.ts b/database/migrations/0036-unique_previous_in_transactions.ts new file mode 100644 index 000000000..f05b044bb --- /dev/null +++ b/database/migrations/0036-unique_previous_in_transactions.ts @@ -0,0 +1,13 @@ +/* MIGRATION TO SET previous COLUMN UNIQUE in TRANSACTION table + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `transactions` ADD UNIQUE(`previous`);') +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `transactions` DROP INDEX `previous`;') +}