diff --git a/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts b/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts new file mode 100644 index 00000000..fea05a0e --- /dev/null +++ b/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts @@ -0,0 +1,136 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AlignTransactionsEntity1775000000000 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'transactions_type_enum') THEN + CREATE TYPE "transactions_type_enum" AS ENUM ('DEPOSIT', 'WITHDRAW', 'SWAP', 'YIELD'); + END IF; + END + $$; + `); + + await queryRunner.query(` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'transactions' AND column_name = 'type' + ) THEN + ALTER TABLE "transactions" + ALTER COLUMN "type" TYPE "transactions_type_enum" + USING ( + CASE + WHEN "type" IN ('DEPOSIT', 'WITHDRAW', 'SWAP', 'YIELD') THEN "type" + ELSE 'YIELD' + END + )::"transactions_type_enum"; + END IF; + END + $$; + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + ALTER COLUMN "amount" TYPE DECIMAL(18,7); + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + ADD COLUMN IF NOT EXISTS "txHash" varchar; + `); + + await queryRunner.query(` + UPDATE "transactions" + SET "txHash" = COALESCE("transactionHash", "eventId", "id"::text) + WHERE "txHash" IS NULL; + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + ALTER COLUMN "txHash" SET NOT NULL; + `); + + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'UQ_transactions_txHash' + ) THEN + ALTER TABLE "transactions" + ADD CONSTRAINT "UQ_transactions_txHash" UNIQUE ("txHash"); + END IF; + END + $$; + `); + + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'transactions_status_enum') THEN + CREATE TYPE "transactions_status_enum" AS ENUM ('COMPLETED', 'PENDING', 'FAILED'); + END IF; + END + $$; + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + ADD COLUMN IF NOT EXISTS "status" "transactions_status_enum" NOT NULL DEFAULT 'COMPLETED'; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "transactions" + DROP COLUMN IF EXISTS "status"; + `); + + await queryRunner.query(` + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'transactions_status_enum') THEN + DROP TYPE "transactions_status_enum"; + END IF; + END + $$; + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + DROP CONSTRAINT IF EXISTS "UQ_transactions_txHash"; + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + DROP COLUMN IF EXISTS "txHash"; + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + ALTER COLUMN "amount" TYPE DECIMAL(20,7); + `); + + await queryRunner.query(` + ALTER TABLE "transactions" + ALTER COLUMN "type" TYPE varchar USING "type"::text; + `); + + await queryRunner.query(` + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'transactions_type_enum') THEN + DROP TYPE "transactions_type_enum"; + END IF; + END + $$; + `); + } +} \ No newline at end of file diff --git a/backend/src/modules/blockchain/entities/transaction.entity.ts b/backend/src/modules/blockchain/entities/transaction.entity.ts index 506f9bc3..a92e0dca 100644 --- a/backend/src/modules/blockchain/entities/transaction.entity.ts +++ b/backend/src/modules/blockchain/entities/transaction.entity.ts @@ -1,51 +1,5 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, - Index, -} from 'typeorm'; - -export enum LedgerTransactionType { - DEPOSIT = 'DEPOSIT', - WITHDRAW = 'WITHDRAW', - YIELD = 'YIELD', -} - -@Entity('transactions') -export class LedgerTransaction { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Index('idx_transactions_user_id') - @Column('uuid') - userId: string; - - @Column({ type: 'enum', enum: LedgerTransactionType }) - type: LedgerTransactionType; - - @Column('decimal', { precision: 20, scale: 7 }) - amount: string; - - @Column({ type: 'varchar', nullable: true }) - publicKey: string | null; - - @Index('idx_transactions_event_id', { unique: true }) - @Column({ type: 'varchar' }) - eventId: string; - - @Column({ type: 'varchar', nullable: true }) - transactionHash: string | null; - - @Column({ type: 'bigint', nullable: true }) - ledgerSequence: string | null; - - @Column({ type: 'varchar', nullable: true }) - poolId: string | null; - - @Column({ type: 'jsonb', nullable: true }) - metadata: Record | null; - - @CreateDateColumn() - createdAt: Date; -} +export { + Transaction as LedgerTransaction, + TxType as LedgerTransactionType, + TxStatus as LedgerTransactionStatus, +} from '../../transactions/entities/transaction.entity'; diff --git a/backend/src/modules/transactions/entities/transaction.entity.ts b/backend/src/modules/transactions/entities/transaction.entity.ts new file mode 100644 index 00000000..57e3514f --- /dev/null +++ b/backend/src/modules/transactions/entities/transaction.entity.ts @@ -0,0 +1,79 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Unique, +} from 'typeorm'; +import { User } from '../../user/entities/user.entity'; + +export enum TxType { + DEPOSIT = 'DEPOSIT', + WITHDRAW = 'WITHDRAW', + SWAP = 'SWAP', + YIELD = 'YIELD', +} + +export enum TxStatus { + COMPLETED = 'COMPLETED', + PENDING = 'PENDING', + FAILED = 'FAILED', +} + +@Entity('transactions') +@Unique(['txHash']) +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user?: User; + + @Index('idx_transactions_user_id') + @Column('uuid') + userId: string; + + @Column({ type: 'enum', enum: TxType }) + type: TxType; + + @Column('decimal', { precision: 18, scale: 7 }) + amount: string; + + @Column({ type: 'varchar' }) + txHash?: string | null; + + @Column({ type: 'enum', enum: TxStatus, default: TxStatus.COMPLETED }) + status?: TxStatus; + + @Column({ type: 'varchar', nullable: true }) + publicKey: string | null; + + @Index('idx_transactions_event_id', { unique: true }) + @Column({ type: 'varchar', nullable: true }) + eventId: string | null; + + @Column({ type: 'bigint', nullable: true }) + ledgerSequence: string | null; + + @Column({ type: 'varchar', nullable: true }) + poolId: string | null; + + @Column({ type: 'jsonb', nullable: true }) + metadata: Record | null; + + @CreateDateColumn() + createdAt: Date; + + get transactionHash(): string | null | undefined { + return this.txHash; + } + + set transactionHash(value: string | null | undefined) { + this.txHash = value; + } +} \ No newline at end of file diff --git a/backend/src/modules/transactions/transactions.service.spec.ts b/backend/src/modules/transactions/transactions.service.spec.ts index f19dbc49..4958a820 100644 --- a/backend/src/modules/transactions/transactions.service.spec.ts +++ b/backend/src/modules/transactions/transactions.service.spec.ts @@ -51,7 +51,7 @@ describe('TransactionsService', () => { describe('findAllForUser', () => { const userId = 'test-user-id'; - const mockTransactions: LedgerTransaction[] = [ + const mockTransactions: Partial[] = [ { id: '1', userId, @@ -74,7 +74,10 @@ describe('TransactionsService', () => { order: Order.DESC, }); - mockQueryBuilder.getManyAndCount.mockResolvedValue([mockTransactions, 1]); + mockQueryBuilder.getManyAndCount.mockResolvedValue([ + mockTransactions as LedgerTransaction[], + 1, + ]); const result = await service.findAllForUser(userId, queryDto);