Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8bc67d5
LED-92 feat: remove system flags from account and operation models
gorushkin Aug 7, 2026
bf0488d
LED-92 feat: add description field to account creation request
gorushkin Aug 16, 2026
b3ea54d
LED-92 feat: update documentation to clarify Jira and PR linking guid…
gorushkin Aug 16, 2026
ee45f3c
Merge pull request #266 from gorushkin:LED-92-remove-balance-fields-f…
gorushkin Aug 17, 2026
04e2db3
LED-96 test: reset transaction manager mock and verify call count in …
gorushkin Aug 17, 2026
2fd939e
Merge pull request #267 from gorushkin:LED-96-move-account-creation-o…
gorushkin Aug 17, 2026
ca7f6eb
LED-98 refactor: decouple account use case repositories by using inte…
gorushkin Aug 17, 2026
4453f87
Merge pull request #268 from gorushkin:LED-98-decouple-account-usecas…
gorushkin Aug 17, 2026
9f99245
LED-112 refactor: unify database schema exports and clean up imports …
gorushkin Aug 17, 2026
166dbec
LED-112 fix: update import statement for CommodityDbRow to use type i…
gorushkin Aug 17, 2026
55f7092
Merge pull request #269 from gorushkin:LED-112-unify-db-schema-exports
gorushkin Aug 17, 2026
a6c9ee4
LED-120 refactor: rename variables for clarity in account filtering t…
gorushkin Aug 17, 2026
31bdb5a
Merge pull request #270 from gorushkin:LED-120-archived-account-statu…
gorushkin Aug 17, 2026
fa283d1
LED-127 test: add TransactionManager tests for nested transaction con…
gorushkin Aug 17, 2026
9b5a9f7
LED-127 refactor: enhance TransactionManager to support transaction c…
gorushkin Aug 17, 2026
e16e615
Merge pull request #271 from gorushkin/LED-127-apply-transaction-boun…
gorushkin Aug 17, 2026
521b60a
LED-131 refactor: unify account ownership checks by integrating ensur…
gorushkin Aug 17, 2026
78ae735
Merge pull request #272 from gorushkin/LED-131-unify-account-ownershi…
gorushkin Aug 17, 2026
2211a3a
LED-135 refactor: replace UserAlreadyExistsError with EntityAlreadyEx…
gorushkin Aug 17, 2026
c11115c
LED-135
gorushkin Aug 17, 2026
0a72e38
Merge pull request #273 from gorushkin/LED-135-split-conflict-errors
gorushkin Aug 17, 2026
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
19 changes: 10 additions & 9 deletions .agents/skills/ledgerly-architecture-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Use this skill when the user asks to:

- create or update ADRs;
- add architectural decision rationale to task docs;
- link Jira/PR to architecture docs;
- link Jira tasks to architecture docs;
- archive completed plan documents;
- replace obsolete decisions and mark superseded status.

Expand Down Expand Up @@ -52,7 +52,10 @@ Every ADR should include:
- `Status: Proposed|Accepted|Superseded|Deprecated`
- `Date: YYYY-MM-DD`
- `Jira: <link-or-N/A>`
- `PR: <link-or-TBD>`

ADR files should not include PR metadata. If a branch reference is useful while
work is in progress, add it under `Related` instead of making it a required
metadata field.

### Minimum ADR sections

Expand All @@ -71,7 +74,7 @@ Keep each section concise and decision-focused.
3. Preserve historical facts; do not rewrite accepted history to match new intent.
4. If direction changed, create new ADR and mark old one as `Superseded`.
5. Keep links stable; when moving plan docs to archive, update ADR links.
6. Prefer explicit links to Jira issue and PR once available.
6. Prefer explicit links to Jira issues; avoid PR metadata in ADR front matter.
7. When updating docs alongside code, keep the doc change scoped to the same
Jira task and explain the code/docs relationship in the final response.

Expand All @@ -93,8 +96,8 @@ For task documents in `docs/plans/`:
2. Create `NNNN-*.md` with required metadata and sections.
3. Add concise rationale and rejected alternatives.
4. Update ADR index in `docs/architecture/adr/README.md`.
5. Add Jira immediately; fill PR as `TBD` if not created.
6. After PR creation, replace `PR: TBD` with actual link.
5. Add Jira immediately. Optionally mention the implementation branch in
`Related` while work is in progress.

### Supersede an ADR

Expand All @@ -107,7 +110,7 @@ For task documents in `docs/plans/`:
### Finalize completed task docs

1. Verify all checklist items are complete.
2. Add/verify Jira and PR references.
2. Add/verify Jira references.
3. Move completed plan to archive if team follows archive policy.
4. Update links in ADR and any docs that referenced old path.

Expand All @@ -132,7 +135,6 @@ Before finalizing documentation changes, confirm:
- Status: Proposed|Accepted|Superseded|Deprecated
- Date: YYYY-MM-DD
- Jira: <link-or-N/A>
- PR: <link-or-TBD>

## Context

Expand Down Expand Up @@ -160,7 +162,6 @@ Before finalizing documentation changes, confirm:
```md
Статус документа: выполнено.

- [x] Связать этот документ с Jira-задачей и итоговым pull request.
- [x] Связать этот документ с Jira-задачей.
Jira: <link>
PR: <link>
```
6 changes: 1 addition & 5 deletions apps/backend/drizzle/0000_nostalgic_ben_parker.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
CREATE TABLE `accounts` (
`commodity_id` text NOT NULL,
`created_at` text NOT NULL,
`current_cleared_balance_local` text NOT NULL,
`description` text NOT NULL,
`id` text PRIMARY KEY NOT NULL,
`initial_balance` text NOT NULL,
`is_closed` integer NOT NULL,
`is_system` integer NOT NULL,
`is_tombstone` integer NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL,
Expand Down Expand Up @@ -41,7 +38,6 @@ CREATE TABLE `operations` (
`created_at` text NOT NULL,
`description` text NOT NULL,
`id` text PRIMARY KEY NOT NULL,
`is_system` integer NOT NULL,
`is_tombstone` integer NOT NULL,
`transaction_id` text NOT NULL,
`updated_at` text NOT NULL,
Expand Down Expand Up @@ -87,4 +83,4 @@ CREATE TABLE `commodities` (
);
--> statement-breakpoint
CREATE UNIQUE INDEX `commodities_user_id_code_unique_idx` ON `commodities` (`user_id`,`code`);--> statement-breakpoint
CREATE UNIQUE INDEX `commodities_user_id_id_unique_idx` ON `commodities` (`user_id`,`id`);
CREATE UNIQUE INDEX `commodities_user_id_id_unique_idx` ON `commodities` (`user_id`,`id`);
30 changes: 1 addition & 29 deletions apps/backend/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
"notNull": true,
"autoincrement": false
},
"current_cleared_balance_local": {
"name": "current_cleared_balance_local",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
Expand All @@ -42,27 +35,13 @@
"notNull": true,
"autoincrement": false
},
"initial_balance": {
"name": "initial_balance",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_closed": {
"name": "is_closed",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_system": {
"name": "is_system",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_tombstone": {
"name": "is_tombstone",
"type": "integer",
Expand Down Expand Up @@ -315,13 +294,6 @@
"notNull": true,
"autoincrement": false
},
"is_system": {
"name": "is_system",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_tombstone": {
"name": "is_tombstone",
"type": "integer",
Expand Down Expand Up @@ -657,4 +629,4 @@
"internal": {
"indexes": {}
}
}
}
6 changes: 3 additions & 3 deletions apps/backend/src/__tests__/user/user.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
EntityAlreadyExistsError,
InvalidPasswordError,
UserAlreadyExistsError,
UserNotFoundError,
} from 'src/application/application.errors';
} from 'src/application';
import { Id } from 'src/domain/domain-core';
import { PasswordManager } from 'src/infrastructure/auth/PasswordManager';
import { UserRepository } from 'src/infrastructure/db/';
Expand Down Expand Up @@ -114,7 +114,7 @@ describe.skip('UserService', () => {
mockUsersRepository.findByEmail.mockResolvedValue({ id: '1' });

await expect(service.update(nextId, userData)).rejects.toThrowError(
UserAlreadyExistsError,
EntityAlreadyExistsError,
);
});

Expand Down
18 changes: 13 additions & 5 deletions apps/backend/src/application/application.errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {
AuthenticationFailedError,
CommodityClosedError,
CommodityHasActiveReferencesError,
EntityAlreadyExistsError,
InvalidPasswordError,
ApplicationError,
EntityNotFoundError,
UserAlreadyExistsError,
UserNotFoundError,
} from 'src/application/application.errors';
import { describe, expect, it } from 'vitest';
Expand Down Expand Up @@ -38,10 +38,18 @@ describe('coded application errors', () => {
},
);

it('uses a stable public code for a registration conflict', () => {
expect(new UserAlreadyExistsError()).toMatchObject({
code: apiErrorCodes.registrationConflict,
context: {},
it('uses a stable public code for an entity that already exists', () => {
expect(
new EntityAlreadyExistsError({
entityType: 'user',
field: 'email',
}),
).toMatchObject({
code: apiErrorCodes.entityAlreadyExists,
context: {
entityType: 'user',
field: 'email',
},
});
});

Expand Down
22 changes: 13 additions & 9 deletions apps/backend/src/application/application.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ export class EntityNotFoundError extends CodedApplicationError<'ENTITY_NOT_FOUND
}
}

/**
* Thrown when an entity violates a public uniqueness contract.
*/
export class EntityAlreadyExistsError extends CodedApplicationError<'ENTITY_ALREADY_EXISTS'> {
constructor(context: ErrorContextByCode['ENTITY_ALREADY_EXISTS']) {
super(
`${context.entityType} already exists for ${context.field}`,
apiErrorCodes.entityAlreadyExists,
context,
);
}
}

/**
* Thrown when a user attempts to access an entity they don't own.
*/
Expand Down Expand Up @@ -131,15 +144,6 @@ export class InvalidPasswordError extends AuthenticationFailedError {
}
}

/**
* Thrown when trying to register a user that already exists.
*/
export class UserAlreadyExistsError extends CodedApplicationError<'REGISTRATION_CONFLICT'> {
constructor(message = 'User already exists') {
super(message, apiErrorCodes.registrationConflict, {});
}
}

/**
* Thrown when an update cannot be applied because aggregate versions differ.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { AccountQuery } from '@ledgerly/shared/validation';
import { AccountSnapshot } from 'src/domain/accounts';

export type AccountRepositoryUpdateInput = Pick<AccountSnapshot, 'updatedAt'> &
Partial<
Pick<AccountSnapshot, 'description' | 'initialBalance' | 'name' | 'type'>
>;
Partial<Pick<AccountSnapshot, 'description' | 'name' | 'type'>>;

export type AccountRepositoryLifecycleInput = { updatedAt: IsoDatetimeString };

Expand Down
8 changes: 1 addition & 7 deletions apps/backend/src/application/mappers/account.mapper.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AccountUpdateDTO } from '@ledgerly/shared/types';
import { AccountSnapshot } from 'src/domain/accounts';
import { Amount, Id, Timestamp } from 'src/domain/domain-core';
import { Id, Timestamp } from 'src/domain/domain-core';
import { describe, expect, it } from 'vitest';

import { AccountMapper } from './account.mapper';
Expand All @@ -9,12 +9,9 @@ describe('AccountMapper', () => {
const snapshot: AccountSnapshot = {
commodityId: Id.create().valueOf(),
createdAt: Timestamp.create().valueOf(),
currentClearedBalanceLocal: Amount.create('2500').valueOf(),
description: 'Operating account',
id: Id.create().valueOf(),
initialBalance: Amount.create('1000').valueOf(),
isClosed: false,
isSystem: true,
isTombstone: false,
name: 'Primary Checking',
type: 'asset',
Expand All @@ -26,12 +23,9 @@ describe('AccountMapper', () => {
expect(AccountMapper.toResponseDTOFromSnapshot(snapshot)).toEqual({
commodityId: snapshot.commodityId,
createdAt: snapshot.createdAt,
currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal,
description: snapshot.description,
id: snapshot.id,
initialBalance: snapshot.initialBalance,
isClosed: snapshot.isClosed,
isSystem: snapshot.isSystem,
isTombstone: snapshot.isTombstone,
name: snapshot.name,
type: snapshot.type,
Expand Down
6 changes: 1 addition & 5 deletions apps/backend/src/application/mappers/account.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
AccountUpdateProps,
CreateAccountProps,
} from 'src/domain/accounts';
import { Amount, Id, Name } from 'src/domain/domain-core';
import { Id, Name } from 'src/domain/domain-core';

export class AccountMapper {
static toResponseDTOFromSnapshot(
Expand All @@ -18,12 +18,9 @@ export class AccountMapper {
return {
commodityId: snapshot.commodityId,
createdAt: snapshot.createdAt,
currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal,
description: snapshot.description,
id: snapshot.id,
initialBalance: snapshot.initialBalance,
isClosed: snapshot.isClosed,
isSystem: snapshot.isSystem,
isTombstone: snapshot.isTombstone,
name: snapshot.name,
type: snapshot.type,
Expand All @@ -35,7 +32,6 @@ export class AccountMapper {
return {
commodityId: Id.restore(dto.commodityId),
description: dto.description,
initialBalance: Amount.create(dto.initialBalance),
name: Name.create(dto.name),
type: AccountType.create(dto.type),
};
Expand Down
1 change: 0 additions & 1 deletion apps/backend/src/application/mappers/operation.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export class OperationMapper {
createdAt: snapshot.createdAt,
description: snapshot.description,
id: snapshot.id,
isSystem: snapshot.isSystem,
transactionId: snapshot.transactionId,
updatedAt: snapshot.updatedAt,
userId: snapshot.userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export class TransactionReadModelResponseMapper {
createdAt: operation.createdAt,
description: operation.description,
id: operation.id,
isSystem: operation.isSystem,
transactionId: operation.transactionId,
updatedAt: operation.updatedAt,
userId: operation.userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ describe('TransactionMapper', () => {
createdAt: activeOperation.createdAt,
description: activeOperation.description,
id: activeOperation.id,
isSystem: activeOperation.isSystem,
transactionId: activeOperation.transactionId,
updatedAt: activeOperation.updatedAt,
userId: activeOperation.userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type OperationReadModel = {
createdAt: IsoDatetimeString;
description: string;
id: UUID;
isSystem: boolean;
transactionId: UUID;
updatedAt: IsoDatetimeString;
userId: UUID;
Expand Down
Loading
Loading