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
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ CREATE TABLE `settings` (
--> statement-breakpoint
CREATE TABLE `entries` (
`created_at` text NOT NULL,
`description` text NOT NULL,
`id` text PRIMARY KEY NOT NULL,
`is_tombstone` integer NOT NULL,
`transaction_id` text NOT NULL,
Expand Down
9 changes: 8 additions & 1 deletion apps/backend/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "ee45f3c0-58ae-4a7e-8165-c14751e8a2d7",
"id": "ea399c7d-e2ca-41c9-8660-10a765e99c05",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"accounts": {
Expand Down Expand Up @@ -519,6 +519,13 @@
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1764699481666,
"tag": "0000_red_red_wolf",
"when": 1765089228039,
"tag": "0000_gifted_professor_monster",
"breakpoints": true
}
]
Expand Down
9 changes: 5 additions & 4 deletions apps/backend/src/application/dto/entry.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {

// Request DTOs for creation

export type CreateEntryRequestDTO = [
CreateOperationRequestDTO,
CreateOperationRequestDTO,
];
export type CreateEntryRequestDTO = {
operations: [CreateOperationRequestDTO, CreateOperationRequestDTO];
description: string;
};

export type EntryOperationsResponseDTO = [
OperationResponseDTO,
Expand All @@ -34,6 +34,7 @@ export type EntryResponseDTO = {
updatedAt: IsoDatetimeString;
operations: EntryOperationsResponseDTO;
isTombstone: boolean;
description: string;
userId: UUID;
};

Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/application/mappers/entry.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class EntryMapper {

return {
createdAt: entry.getCreatedAt().valueOf(),
description: entry.description,
id: entry.getId().valueOf(),
isTombstone: entry.isDeleted(),
operations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
TransactionResponseDTO,
} from '../dto';

// TODO: This mapper is similar to TransactionMapper but works with DB rows directly.
// Consider refactoring to reduce code duplication.
export class TransactionViewMapper {
static toView(transaction: TransactionWithRelations): TransactionResponseDTO {
return {
Expand Down Expand Up @@ -55,6 +57,7 @@ export class TransactionViewMapper {

return {
createdAt: entry.createdAt,
description: entry.description,
id: entry.id,
isTombstone: entry.isTombstone,
operations,
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/application/mappers/transaction.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class TransactionMapper implements TransactionMapperInterface {

return {
createdAt: entryData.createdAt,
description: entryData.description,
id: entryData.id,
isTombstone: entryData.isTombstone,
operations: [userOperations[0], userOperations[1]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('EntryFactory', () => {
AccountType.create('asset'),
);

mockEntry = Entry.create(user, transaction);
mockEntry = Entry.create(user, transaction, 'Mock Entry 1');

operationFrom = Operation.create(
user,
Expand All @@ -111,18 +111,21 @@ describe('EntryFactory', () => {
const mockOperations = [operationFrom, operationTo];

const rawEntries: CreateEntryRequestDTO[] = [
[
{
accountId: account1.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
{
accountId: account2.getId().valueOf(),
amount: Amount.create('-100').valueOf(),
description: 'Operation 2',
},
],
{
description: mockEntry.description,
operations: [
{
accountId: operationFrom.getAccountId().valueOf(),
amount: operationFrom.amount.valueOf(),
description: operationFrom.description,
},
{
accountId: operationTo.getAccountId().valueOf(),
amount: operationTo.amount.valueOf(),
description: operationTo.description,
},
],
},
];

mockAccountRepository.getByIds.mockResolvedValueOnce([
Expand All @@ -144,7 +147,45 @@ describe('EntryFactory', () => {
rawEntries,
);

expect(result).toHaveLength(1);
expect(result).toHaveLength(rawEntries.length);

const checkedEntries = new Set(rawEntries.map((e) => e.description));

result.forEach((entry) => {
const rawEntry = rawEntries.find((e) => {
return e.description === entry.description;
});

if (rawEntry) {
checkedEntries.delete(rawEntry.description);
expect(entry.description).toBe(rawEntry.description);
}

expect(entry).toBeInstanceOf(Entry);

const checkedOperations = new Set(
rawEntry?.operations.map((op) => op.accountId),
);

entry.getOperations().forEach((operation) => {
const rawOperation = rawEntry?.operations.find((op) => {
return op.accountId === operation.getAccountId().valueOf();
});

if (rawOperation) {
checkedOperations.delete(rawOperation.accountId);

expect(operation.getAccountId().valueOf()).toBe(
rawOperation.accountId,
);
expect(operation.amount.valueOf()).toBe(rawOperation.amount);
expect(operation.description).toBe(rawOperation.description);
}
});
expect(checkedOperations.size).toBe(0);
});

expect(checkedEntries.size).toBe(0);

const entry = result[0];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ describe('OperationFactory', () => {
description: 'Operation to',
};

const operationsRaw: CreateEntryRequestDTO = [
operationFromDTO,
operationToDTO,
];
const entriesRaw: CreateEntryRequestDTO = {
description: 'Test Entry',
operations: [operationFromDTO, operationToDTO],
};

const mockedOperationFrom = Operation.create(
user,
Expand Down Expand Up @@ -142,20 +142,20 @@ describe('OperationFactory', () => {
const operations = await operationFactory.createOperationsForEntry(
user,
entry,
operationsRaw,
entriesRaw,
accountsByIdMap,
currencySystemAccountsMap,
);

expect(mockedSaveWithIdRetry).toHaveBeenCalledTimes(
Object.keys(operationsRaw).length,
entriesRaw.operations.length,
);

operations.forEach((operation, index) => {
expect(operation).toBeInstanceOf(Operation);
expect(operation.getId()).toBeDefined();
expect(operation.belongsToUser(user.getId())).toBe(true);
const expectedData = operationsRaw[index];
const expectedData = entriesRaw.operations[index];
expect(operation.amount.valueOf()).toBe(expectedData.amount);
expect(operation.description).toBe(expectedData.description);
});
Expand All @@ -170,22 +170,25 @@ describe('OperationFactory', () => {
expect(operation).toBeInstanceOf(Operation);
});

expect(operations).toHaveLength(Object.keys(operationsRaw).length);
expect(operations).toHaveLength(entriesRaw.operations.length);
});

it('should throw an error if account not found', async () => {
const operationsRaw: CreateEntryRequestDTO = [
{
accountId: usdAccount1.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
{
accountId: usdAccount2.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
];
const entriesRaw: CreateEntryRequestDTO = {
description: 'Test Entry',
operations: [
{
accountId: usdAccount1.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
{
accountId: usdAccount2.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
],
};

// Create accountsMap with missing account
const accountsMap = new Map([
Expand All @@ -202,12 +205,12 @@ describe('OperationFactory', () => {
operationFactory.createOperationsForEntry(
user,
entry,
operationsRaw,
entriesRaw,
accountsMap,
currencySystemAccountsMap,
),
).rejects.toThrowError(
`Account not found in map: ${operationsRaw[0].accountId}`,
`Account not found in map: ${entriesRaw.operations[0].accountId}`,
);
});

Expand Down Expand Up @@ -240,16 +243,19 @@ describe('OperationFactory', () => {
},
};

const operationsRaw: CreateEntryRequestDTO = [
{
...testData.from.operation,
amount: testData.from.amount.valueOf(),
},
{
...testData.to.operation,
amount: testData.to.amount.valueOf(),
},
];
const entryOperationsInput: CreateEntryRequestDTO = {
description: 'Test Entry',
operations: [
{
...testData.from.operation,
amount: testData.from.amount.valueOf(),
},
{
...testData.to.operation,
amount: testData.to.amount.valueOf(),
},
],
};

// Create accountsMap for the new signature (only user accounts, system accounts will be fetched separately)
const accountsMap = new Map([
Expand Down Expand Up @@ -303,7 +309,7 @@ describe('OperationFactory', () => {
const operations = await operationFactory.createOperationsForEntry(
user,
entry,
operationsRaw,
entryOperationsInput,
accountsMap,
currencySystemAccountsMap,
);
Expand Down Expand Up @@ -344,7 +350,7 @@ describe('OperationFactory', () => {
expect(operation.currency.valueOf()).toBe(match.currency.valueOf());
});

for (let i = 0; i < operationsRaw.length; i++) {
for (let i = 0; i < entryOperationsInput.operations.length; i++) {
const resultOperation = operations[i];
const systemOperation = operations[i + 2];
const systemAmount = Amount.fromPersistence(
Expand All @@ -358,18 +364,21 @@ describe('OperationFactory', () => {
});

it('should throw an error if the balances of operations do not equal zero', async () => {
const operationsRaw: CreateEntryRequestDTO = [
{
accountId: usdAccount1.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
{
accountId: usdAccount2.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
];
const entriesRaw: CreateEntryRequestDTO = {
description: 'Test Entry',
operations: [
{
accountId: usdAccount1.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
{
accountId: usdAccount2.getId().valueOf(),
amount: Amount.create('100').valueOf(),
description: 'Operation 1',
},
],
};

// Create accountsMap for the new signature
const accountsMap = new Map([
Expand All @@ -385,7 +394,7 @@ describe('OperationFactory', () => {
operationFactory.createOperationsForEntry(
user,
entry,
operationsRaw,
entriesRaw,
accountsMap,
currencySystemAccountsMap,
),
Expand Down
10 changes: 6 additions & 4 deletions apps/backend/src/application/services/entry.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export class EntryFactory {
const accountIds = new Set<UUID>();
const currenciesSet = new Set<CurrencyCode>();

for (const [from, to] of entries) {
accountIds.add(from.accountId);
accountIds.add(to.accountId);
for (const entry of entries) {
for (const operation of entry.operations) {
accountIds.add(operation.accountId);
}
}

const accountRows = await this.accountRepository.getByIds(
Expand Down Expand Up @@ -104,7 +105,8 @@ export class EntryFactory {
accountsMap: Map<UUID, Account>,
systemAccountsMap: Map<CurrencyCode, Account>,
): Promise<Entry> {
const createEntry = () => Entry.create(user, transaction);
const createEntry = () =>
Entry.create(user, transaction, entryData.description);

const entry = await this.saveEntry(createEntry);

Expand Down
Loading