From be90889ee94450100b2d743bc168277e3e2b924b Mon Sep 17 00:00:00 2001 From: gorushkin Date: Wed, 15 Jul 2026 13:37:09 +0300 Subject: [PATCH 01/30] LED-65 docs: define commodity domain model and update ADR references --- docs/DATABASE_SCHEMA.md | 26 +++- docs/DOMAIN.md | 46 +++---- docs/MULTICURRENCY_DESIGN.md | 13 +- ...set-registry-before-currency-validation.md | 78 ----------- ...ity-registry-before-currency-validation.md | 125 ++++++++++++++++++ docs/architecture/adr/README.md | 2 +- 6 files changed, 173 insertions(+), 117 deletions(-) delete mode 100644 docs/architecture/adr/0006-asset-registry-before-currency-validation.md create mode 100644 docs/architecture/adr/0006-commodity-registry-before-currency-validation.md diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 56d9be4c..412277a7 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -151,14 +151,12 @@ User's financial accounts for tracking funds. --- ### 💱 **Currencies** -Dictionary of supported currencies. +Legacy dictionary of supported currencies. -> Design note: this table is an MVP currency dictionary, not the final identity -> model for all monetary units. Ledgerly is expected to need an asset/commodity -> registry for fiat currencies, crypto assets, network-specific tokens, and -> custom user assets before final transaction currency existence validation is -> implemented. See -> [ADR 0006](./architecture/adr/0006-asset-registry-before-currency-validation.md). +> Design note: this table is not the final identity model for monetary units. +> The target model is a user-owned `commodities` table. Commodity identity is a +> stable id, not `code`. See +> [ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). | Field | Type | Description | Constraints | |------|-----|----------|-------------| @@ -175,6 +173,20 @@ Dictionary of supported currencies. - `1:N` with `accounts` - `1:N` with `settings` +**Target Commodity model:** + +| Field | Type | Description | Constraints | +|------|-----|----------|-------------| +| `id` | UUID | Stable Commodity identity | PK | +| `userId` | UUID | Commodity owner | FK -> `users.id`, NOT NULL | +| `code` | String | Display/search code | NOT NULL, unique per user | +| `name` | String | Display name | NOT NULL | +| `symbol` | String | Display symbol | NOT NULL | +| `precision` | Integer | Minor-unit scale for integer amounts | NOT NULL, immutable after creation | +| `isArchived` | Boolean | Hidden from new assignments | NOT NULL, default: false | +| `createdAt` | Timestamp | Creation time | NOT NULL | +| `updatedAt` | Timestamp | Last update time | NOT NULL | + --- ### 📝 **Transactions** diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index 45c7218b..f7bcf135 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -64,24 +64,24 @@ Represents different financial accounts with unified structure for all account t - `isSystem = true` is reserved for future system trading accounts (currently unused) - Has soft delete support (`isTombstone`) -### Currency +### Commodity -Represents different monetary units used in the system. +Represents a user-owned monetary unit used in the system. -> Design note: this concept is expected to evolve into an asset/commodity -> registry before transaction currency existence validation is finalized. Fiat -> currencies, crypto assets, tokenized assets on different networks, and custom -> user-defined assets should not all rely on a currency code as primary -> identity. See -> [ADR 0006](./architecture/adr/0006-asset-registry-before-currency-validation.md). +> Design note: the current implementation still uses currency strings in some +> places, but the target domain model is a user-owned Commodity Registry. +> Commodity identity is a stable id, not `code`. See +> [ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). #### Key Properties -- Each account has a designated currency -- System has a base currency for reporting -- Operations always store amounts in the account's currency -- Currency conversion is handled through trading operations -- Historical rates can be stored for accurate reporting +- Each account has a designated Commodity +- Account Commodity is immutable after account creation +- Transactions have a valuation Commodity that determines `value` denomination +- Operations always store `amount` in the account's Commodity +- Commodity `code`, `name`, and `symbol` are display metadata, not identity +- Commodity `precision` defines integer minor-unit interpretation +- Archived Commodities remain readable but cannot be used for new assignments ## Entity API Conventions @@ -379,22 +379,20 @@ Account - createdAt: timestamp - updatedAt: timestamp -Currency -- code: string (PK) +Commodity (target model, see ADR 0006) +- id: UUID +- userId: UUID (FK) +- code: string -- display/search value, unique per user - name: string - symbol: string - -Future asset registry direction: -- id: UUID or stable asset identifier -- type: fiat | crypto | commodity | custom -- code/symbol: display and search values, not necessarily globally unique -- precision: minor-unit scale for integer amount storage -- network and contract address: optional metadata for tokenized assets -- createdByUserId: nullable owner for user-defined assets +- precision: integer -- immutable after creation in the MVP +- isArchived: boolean +- createdAt: timestamp +- updatedAt: timestamp Settings - userId: UUID (PK, FK) -- baseCurrency: string (FK) +- baseCurrency: string (FK, legacy; target model should use Commodity id) - createdAt: timestamp - updatedAt: timestamp ``` diff --git a/docs/MULTICURRENCY_DESIGN.md b/docs/MULTICURRENCY_DESIGN.md index 0572acba..9d70ebde 100644 --- a/docs/MULTICURRENCY_DESIGN.md +++ b/docs/MULTICURRENCY_DESIGN.md @@ -128,17 +128,16 @@ Trading accounts will never be displayed to the user. ## 5. Data Structure -> Design note: the current schema uses `currency` strings, but this should not -> be treated as the final persistence model for monetary units. Before adding -> existence validation for transaction currency, define an asset/commodity -> registry that can represent fiat currencies, crypto assets, tokens on specific -> networks, and custom user assets. See -> [ADR 0006](./architecture/adr/0006-asset-registry-before-currency-validation.md). +> Design note: the current schema uses `currency` strings, but the target model +> is a user-owned Commodity Registry. Commodity identity is a stable id, not a +> display code. See +> [ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). ### Transaction Top-level entity representing a financial event: - `id`, `userId`, `description` -- `currency` (base currency for the transaction — determines `value` denomination) +- `currency` today; target model: `valuationCommodityId`, which determines + `value` denomination - `transactionDate`, `postingDate` - `version` (optimistic concurrency) - `createdAt`, `updatedAt`, `isTombstone` diff --git a/docs/architecture/adr/0006-asset-registry-before-currency-validation.md b/docs/architecture/adr/0006-asset-registry-before-currency-validation.md deleted file mode 100644 index 589c4faa..00000000 --- a/docs/architecture/adr/0006-asset-registry-before-currency-validation.md +++ /dev/null @@ -1,78 +0,0 @@ -# ADR 0006: Asset Registry Before Currency Validation - -- Status: Proposed -- Date: 2026-06-20 -- Jira: https://gorushkin.atlassian.net/browse/LED-64 -- PR: TBD - -## Context - -LED-47 asks for transaction currency consistency: a transaction currency must be -present and must exist in the system. The current database has a `currencies` -table keyed by currency code, while `accounts.currency` and -`transactions.currency` are plain text fields without foreign keys. - -That model is too narrow for the planned Ledgerly domain. Ledgerly needs to -represent fiat currencies, crypto assets, tokenized assets on different -networks, and possibly user-defined assets. In that domain, a display code such -as `USDT` is not a stable primary identifier because the same symbol can exist -on multiple networks or collide with user-defined assets. - -## Decision - -Do not implement LED-47 as final validation against the existing -`currencies.code` model. - -Before enforcing transaction currency existence, introduce or design a broader -asset registry model. The registry should treat fiat currencies as one asset -type rather than the only supported monetary unit. - -The future registry should support at least: - -- stable asset identity independent of display code; -- asset type, such as `fiat`, `crypto`, `commodity`, or `custom`; -- precision/decimal scale for integer minor-unit storage; -- optional network and contract address for token assets; -- active/disabled state; -- ownership or availability rules for user-defined assets. - -Transaction, account, and settings currency references should eventually point -to asset identifiers rather than relying only on text currency codes. - -## Alternatives Considered - -1. Validate LED-47 against `currencies.code` now - -- Pros: small change, closes the immediate task. -- Cons: reinforces a model already known to be insufficient for crypto and - custom assets; likely requires rework when asset identity is introduced. - -2. Add foreign keys from accounts and transactions to `currencies.code` - -- Pros: improves relational consistency for the current schema. -- Cons: still assumes ISO-like currency codes are the primary identity and does - not solve token/network collisions. - -3. Keep validation only in request schemas - -- Pros: minimal work. -- Cons: does not protect application use cases called outside HTTP and does not - ensure referenced monetary units exist in the system. - -## Consequences - -- LED-47 should be deferred or re-scoped until the asset registry decision is - accepted. -- Short-term validation may continue to check syntactic currency-code shape, but - should not be treated as final existence validation. -- Future migrations will need to preserve existing `USD`, `EUR`, and similar - rows as fiat assets. -- Amount validation should eventually use asset precision rather than assuming - all money has two decimal places. - -## Related - -- [LED-47: Currency Consistency (MVP)](https://gorushkin.atlassian.net/browse/LED-47) -- [Multicurrency Design](../../MULTICURRENCY_DESIGN.md) -- [Domain Model](../../DOMAIN.md) -- [Database Schema](../../DATABASE_SCHEMA.md) diff --git a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md new file mode 100644 index 00000000..3d978347 --- /dev/null +++ b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md @@ -0,0 +1,125 @@ +# ADR 0006: Commodity Registry Before Currency Validation + +- Status: Accepted +- Date: 2026-07-15 +- Jira: https://gorushkin.atlassian.net/browse/LED-65 +- PR: TBD + +## Context + +LED-47 asks for transaction currency consistency: a transaction currency must be +present and must exist in the system. The current database has a `currencies` +table keyed by currency code, while `accounts.currency` and +`transactions.currency` are plain text fields without foreign keys. + +That model is too narrow for the planned Ledgerly domain. Ledgerly is inspired +by GnuCash, where monetary units are modeled as commodities. Fiat currencies are +commodities, but the same abstraction can also represent crypto assets, +tokenized assets, points, or user-defined units. + +In this domain, a display code such as `USD` or `USDT` is not stable identity: +codes can be edited by the user, can collide across users, and can collide +across networks or custom assets. Ledgerly also needs a path to user-created and +user-edited monetary units, so `Currency` should not remain a closed value +object with predefined values. + +## Decision + +Do not implement LED-47 as final validation against the existing +`currencies.code` model. Introduce a user-owned Commodity Registry first. + +`Commodity` is the backend/domain term for a monetary unit. Product copy and UI +may still use words such as currency, asset, or unit where that is clearer for a +user, but domain code should converge on Commodity. + +The Commodity model is: + +- each Commodity has a stable `id` and `userId`; +- Commodity identity is `id`, not `code`; +- predefined `USD`, `EUR`, and `RUB` are created per user as ordinary + Commodities, not global records; +- `code`, `name`, and `symbol` are editable display/search metadata; +- `code` is unique only within one user's Commodity registry; +- `precision` defines integer minor-unit interpretation and is immutable after + Commodity creation in the MVP; +- Commodity can be archived; archived Commodities cannot be assigned to new + accounts or new transaction valuation units, but existing references remain + readable. + +Domain references should move from currency strings to Commodity ids: + +- `Account` references `commodityId`; +- account Commodity is immutable after account creation; if the user selected + the wrong Commodity, they create a new account; +- `Transaction` references `valuationCommodityId`; +- `Operation.amount` is denominated in the account Commodity; +- `Operation.value` is denominated in the transaction valuation Commodity; +- transaction balance validation continues to use the sum of active + `Operation.value` values. + +The existing `/currencies` HTTP stub is not the target API and should be +removed during the Commodity migration rather than implemented on top of the old +model. + +The MVP explicitly excludes global reference assets, market data providers, +exchange-rate history, automatic conversion, and network/contract metadata. +Those can be introduced by later ADRs if needed. + +## Alternatives Considered + +1. Validate LED-47 against `currencies.code` now + +- Pros: small change, closes the immediate task. +- Cons: reinforces a model already known to be insufficient; makes display code + the identity even though Commodity identity must be stable and user-owned. + +2. Add foreign keys from accounts and transactions to `currencies.code` + +- Pros: improves relational consistency for the current schema. +- Cons: still assumes ISO-like currency codes are the primary identity and does + not support editable, user-owned Commodities. + +3. Keep validation only in request schemas + +- Pros: minimal work. +- Cons: does not protect application use cases called outside HTTP and does not + ensure referenced monetary units exist in the system. + +4. Introduce a global asset registry first + +- Pros: closer to a future market data and reference-asset model. +- Cons: too large for the current product step and conflicts with the immediate + need for user-created, user-edited Commodities. + +## Consequences + +- LED-47 is re-scoped: final consistency validation must use Commodity + existence, ownership, and archive state instead of `currencies.code`. +- The current `Currency` value object should stop being domain identity. During + migration it may survive only as a primitive code/format helper if useful. +- Persistence needs a `commodities` model and a migration path from existing + currency strings to per-user Commodity records. +- Account creation must require a Commodity; account Commodity changes are not + allowed after creation. +- Transaction creation must require a valuation Commodity. +- Existing API and DTO surfaces may need compatibility fields while data and + clients migrate from currency strings to Commodity ids. +- Amount validation should use Commodity precision rather than assuming all + amounts have two decimal places. + +## Related + +- [LED-64: Commodity Registry Epic](https://gorushkin.atlassian.net/browse/LED-64) +- [LED-65: Define Commodity domain model and ADR](https://gorushkin.atlassian.net/browse/LED-65) +- [LED-47: Currency Consistency (MVP)](https://gorushkin.atlassian.net/browse/LED-47) +- [LED-109: Add Commodity domain primitives](https://gorushkin.atlassian.net/browse/LED-109) +- [LED-66: Design Commodity schema and migration path](https://gorushkin.atlassian.net/browse/LED-66) +- [LED-67: Implement Commodity persistence, repository, and defaults](https://gorushkin.atlassian.net/browse/LED-67) +- [LED-68: Move Account to immutable Commodity reference](https://gorushkin.atlassian.net/browse/LED-68) +- [LED-111: Move transaction valuation to Commodity reference](https://gorushkin.atlassian.net/browse/LED-111) +- [LED-69: Validate Commodity existence, ownership, and archive state](https://gorushkin.atlassian.net/browse/LED-69) +- [LED-105: Remove `/currencies` endpoint stub](https://gorushkin.atlassian.net/browse/LED-105) +- [LED-70: Replace LED-47 validation with Commodity validation](https://gorushkin.atlassian.net/browse/LED-70) +- [Multicurrency Design](../../MULTICURRENCY_DESIGN.md) +- [Domain Model](../../DOMAIN.md) +- [Database Schema](../../DATABASE_SCHEMA.md) diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index f07e91a1..113e87b8 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -36,7 +36,7 @@ - [ADR 0003: No zero-net account effect invariant](./0003-no-zero-net-account-effect-invariant.md) - [ADR 0004: No minimum distinct accounts invariant](./0004-no-minimum-distinct-accounts-invariant.md) - [ADR 0005: No non-zero operation invariant](./0005-no-non-zero-operation-invariant.md) -- [ADR 0006: Asset registry before currency validation](./0006-asset-registry-before-currency-validation.md) +- [ADR 0006: Commodity registry before currency validation](./0006-commodity-registry-before-currency-validation.md) - [ADR 0007: No operation amount sign invariant](./0007-no-operation-amount-sign-invariant.md) - [ADR 0008: Structured API error contract](./0008-structured-api-error-contract.md) - [ADR 0009: Repository error-code migration](./0009-repository-error-code-migration.md) From 73236e1f189bd10f43c4ce6706eee6364640cc49 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Wed, 15 Jul 2026 16:16:57 +0300 Subject: [PATCH 02/30] LED-109 feat(commodities): add commodity domain primitives and validation --- .../domain/commodities/commodity.entity.ts | 187 ++++++++++ .../src/domain/commodities/commodity.test.ts | 345 ++++++++++++++++++ .../backend/src/domain/commodities/helpers.ts | 34 ++ apps/backend/src/domain/commodities/index.ts | 1 + apps/backend/src/domain/commodities/types.ts | 23 ++ .../value-objects/CommodityCode.test.ts | 89 +++++ .../value-objects/CommodityCode.ts | 34 ++ .../domain/domain-core/value-objects/index.ts | 1 + apps/backend/src/domain/domain.errors.ts | 51 +++ apps/backend/src/domain/index.ts | 1 + .../src/presentation/http/error-handler.ts | 3 + packages/shared/src/types/error-contract.ts | 18 + packages/shared/src/types/types.ts | 7 + .../shared/src/validation/baseValidations.ts | 30 ++ 14 files changed, 824 insertions(+) create mode 100644 apps/backend/src/domain/commodities/commodity.entity.ts create mode 100644 apps/backend/src/domain/commodities/commodity.test.ts create mode 100644 apps/backend/src/domain/commodities/helpers.ts create mode 100644 apps/backend/src/domain/commodities/index.ts create mode 100644 apps/backend/src/domain/commodities/types.ts create mode 100644 apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts create mode 100644 apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts diff --git a/apps/backend/src/domain/commodities/commodity.entity.ts b/apps/backend/src/domain/commodities/commodity.entity.ts new file mode 100644 index 00000000..a3c12473 --- /dev/null +++ b/apps/backend/src/domain/commodities/commodity.entity.ts @@ -0,0 +1,187 @@ +import { CommodityPrecisionNumber } from '@ledgerly/shared/types'; + +import { + CommodityCode, + EntityIdentity, + EntityTimestamps, + Id, + Name, + ParentChildRelation, + SoftDelete, + Timestamp, +} from '../domain-core'; +import { DeletedEntityOperationError } from '../domain.errors'; +import { User } from '../users/user.entity'; + +import { parseCommodityPrecision, parseCommoditySymbol } from './helpers'; +import type { CommoditySnapshot, CommodityUpdateProps } from './types'; + +export class Commodity { + static readonly entityType = 'commodity'; + private constructor( + private readonly identity: EntityIdentity, + private timestamps: EntityTimestamps, + private softDelete: SoftDelete, + private readonly ownership: ParentChildRelation, + private code: CommodityCode, + public symbol: string | null, + public name: Name, + private precision: CommodityPrecisionNumber, + ) {} + + static create( + user: User, + name: Name, + code: CommodityCode, + precision = 2, + symbol: string | null = null, + ): Commodity { + const identity = EntityIdentity.create(); + const softDelete = SoftDelete.create(); + const timestamps = EntityTimestamps.create(); + + const ownership = ParentChildRelation.create( + user.getId(), + identity.getId(), + ); + + const parsedCommoditySymbol = parseCommoditySymbol(symbol); + const parsedCommodityPrecision = parseCommodityPrecision(precision); + + return new Commodity( + identity, + timestamps, + softDelete, + ownership, + code, + parsedCommoditySymbol, + name, + parsedCommodityPrecision, // use the provided precision value + ); + } + + static restore(data: CommoditySnapshot): Commodity { + const { + code, + createdAt, + id, + isTombstone, + precision, + symbol, + updatedAt, + userId, + } = data; + + const identity = EntityIdentity.restore(Id.restore(id)); + + const timestamps = EntityTimestamps.restore( + Timestamp.restore(updatedAt), + Timestamp.restore(createdAt), + ); + + const softDelete = SoftDelete.restore(isTombstone); + + const ownership = ParentChildRelation.create( + Id.restore(userId), + identity.getId(), + ); + + const commodityCode = CommodityCode.restore(code); + const commodityName = Name.restore(data.name); + + const parsedCommoditySymbol = parseCommoditySymbol(symbol); + const parsedCommodityPrecision = parseCommodityPrecision(precision); + + return new Commodity( + identity, + timestamps, + softDelete, + ownership, + commodityCode, + parsedCommoditySymbol, + commodityName, + parsedCommodityPrecision, + ); + } + + getId(): Id { + return this.identity.getId(); + } + + // Delegation methods for timestamps + getUpdatedAt(): Timestamp { + return this.timestamps.getUpdatedAt(); + } + + getCreatedAt(): Timestamp { + return this.timestamps.getCreatedAt(); + } + + private touch(now?: Timestamp): void { + this.timestamps = this.timestamps.touch(now); + } + + markAsDeleted(): void { + this.softDelete = this.softDelete.markAsDeleted( + new DeletedEntityOperationError(Commodity.entityType, 'delete'), + ); + this.touch(); + } + + isDeleted(): boolean { + return this.softDelete.isDeleted(); + } + + private validateUpdateIsAllowed(): void { + this.softDelete.validateUpdateIsAllowed( + DeletedEntityOperationError.forUpdate(Commodity.entityType), + ); + } + + belongsToUser(userId: Id): boolean { + return this.ownership.belongsToParent(userId); + } + + getUserId(): Id { + return this.ownership.getParentId(); + } + + toSnapshot(): CommoditySnapshot { + return { + code: this.code.valueOf(), + createdAt: this.getCreatedAt().valueOf(), + id: this.getId().valueOf(), + isTombstone: this.softDelete.getIsTombstone(), + name: this.name.valueOf(), + precision: this.precision, + symbol: this.symbol, + updatedAt: this.getUpdatedAt().valueOf(), + userId: this.getUserId().valueOf(), + }; + } + + update(data: CommodityUpdateProps): void { + this.validateUpdateIsAllowed(); + + let isUpdated = false; + + if (data.code !== undefined) { + this.code = CommodityCode.create(data.code); + isUpdated = true; + } + + if (data.symbol !== undefined) { + this.symbol = parseCommoditySymbol(data.symbol); + isUpdated = true; + } + + if (data.name !== undefined) { + this.name = Name.create(data.name); + isUpdated = true; + } + + if (isUpdated) { + this.touch(); + } + } +} diff --git a/apps/backend/src/domain/commodities/commodity.test.ts b/apps/backend/src/domain/commodities/commodity.test.ts new file mode 100644 index 00000000..d11ffbfd --- /dev/null +++ b/apps/backend/src/domain/commodities/commodity.test.ts @@ -0,0 +1,345 @@ +import { createUser } from 'src/testing'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; + +import { CommodityCode, Id, Name, Timestamp } from '../domain-core'; +import { + DeletedEntityOperationError, + InvalidCommodityPrecisionError, + InvalidCommoditySymbolError, +} from '../domain.errors'; +import { User } from '../users/user.entity'; + +import { Commodity } from './commodity.entity'; +import { CommoditySnapshot } from './types'; + +describe('Commodity Domain Entity', () => { + let user: User; + let userId: ReturnType; + + const commodityCode = CommodityCode.create('commodityCode'); + const commoditySymbol = 'CMD'; + const commodityName = Name.create('Commodity Name'); + const commodityPrecision = 2; + + beforeAll(async () => { + user = await createUser(); + userId = user.getId(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('create method', () => { + it('should create commodity with valid data', () => { + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + expect(commodity).toBeInstanceOf(Commodity); + expect(commodity.getId()).toBeDefined(); + expect(commodity.getUserId().valueOf()).toBe(userId.valueOf()); + expect(commodity).toHaveProperty('code', commodityCode); + expect(commodity).toHaveProperty('symbol', commoditySymbol); + expect(commodity.getUserId().equals(userId)).toBe(true); + }); + + it('should create commodity with default precision and null symbol', () => { + const commodity = Commodity.create(user, commodityName, commodityCode); + + expect(commodity).toBeInstanceOf(Commodity); + expect(commodity.getId()).toBeDefined(); + expect(commodity.getUserId().valueOf()).toBe(userId.valueOf()); + expect(commodity).toHaveProperty('code', commodityCode); + expect(commodity).toHaveProperty('symbol', null); + expect(commodity.getUserId().equals(userId)).toBe(true); + }); + + it('should create commodity with specified precision and null symbol', () => { + const specifiedPrecision = 4; + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + specifiedPrecision, + ); + + expect(commodity).toBeInstanceOf(Commodity); + expect(commodity.getId()).toBeDefined(); + expect(commodity.getUserId().valueOf()).toBe(userId.valueOf()); + expect(commodity).toHaveProperty('code', commodityCode); + expect(commodity).toHaveProperty('symbol', null); + expect(commodity.getUserId().equals(userId)).toBe(true); + }); + + it('should throw error when creating commodity with invalid precision', () => { + const invalidPrecision = -1; + + expect(() => { + Commodity.create(user, commodityName, commodityCode, invalidPrecision); + }).toThrowError(InvalidCommodityPrecisionError); + }); + + it('should throw error when creating commodity with invalid symbol', () => { + const invalidSymbol = 'INVALID_SYMBOL'; + + expect(() => { + Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + invalidSymbol, + ); + }).toThrowError(InvalidCommoditySymbolError); + }); + }); + + describe('restore method', () => { + it('should restore commodity from snapshot', () => { + const createdAtValue = '2023-10-01T12:00:00.000Z'; + const updatedAtValue = '2023-10-02T12:00:00.000Z'; + const commodityIdValue = '223e4567-e89b-12d3-a456-426614174000'; + + const createdAt = Timestamp.restore(createdAtValue); + const updatedAt = Timestamp.restore(updatedAtValue); + + const commodityId = Id.restore(commodityIdValue); + + const snapshot: CommoditySnapshot = { + code: commodityCode.valueOf(), + createdAt: createdAt.valueOf(), + id: commodityId.valueOf(), + isTombstone: false, + name: commodityName.valueOf(), + precision: commodityPrecision, + symbol: 'RCMD', + updatedAt: updatedAt.valueOf(), + userId: userId.valueOf(), + }; + + const restoredCommodity = Commodity.restore(snapshot); + + expect(restoredCommodity).toBeInstanceOf(Commodity); + expect(restoredCommodity.getId().valueOf()).toBe(snapshot.id); + expect(restoredCommodity.getUserId().valueOf()).toBe(snapshot.userId); + expect(restoredCommodity).toHaveProperty( + 'code', + CommodityCode.create(snapshot.code), + ); + + expect(restoredCommodity.symbol).toBe(snapshot.symbol); + expect(restoredCommodity.belongsToUser(userId)).toBe(true); + }); + + it('should throw error when restoring commodity with invalid precision', () => { + const snapshot: CommoditySnapshot = { + code: commodityCode.valueOf(), + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: commodityName.valueOf(), + precision: -1, // Invalid precision + symbol: 'RCMD', + updatedAt: Timestamp.create().valueOf(), + userId: userId.valueOf(), + }; + + expect(() => { + Commodity.restore(snapshot); + }).toThrowError(InvalidCommodityPrecisionError); + }); + + it('should throw error when restoring commodity with invalid symbol', () => { + const snapshot: CommoditySnapshot = { + code: commodityCode.valueOf(), + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: commodityName.valueOf(), + precision: commodityPrecision, + symbol: 'INVALID_SYMBOL', // Invalid symbol + updatedAt: Timestamp.create().valueOf(), + userId: userId.valueOf(), + }; + + expect(() => { + Commodity.restore(snapshot); + }).toThrowError(); + }); + }); + + describe('commodity management', () => { + describe('markAsDeleted and isDeleted methods', () => { + it('should mark commodity as deleted and check deletion status', () => { + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + expect(commodity.isDeleted()).toBe(false); + + commodity.markAsDeleted(); + + expect(commodity.isDeleted()).toBe(true); + }); + + it('should not allow updates on a deleted commodity', () => { + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + commodity.markAsDeleted(); + + const newCode = CommodityCode.create('updatedCode'); + + expect(() => { + commodity.update({ code: newCode.valueOf(), symbol: 'NEW' }); + }).toThrow(DeletedEntityOperationError); + }); + + it('should not allow deleting an already deleted commodity', () => { + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + commodity.markAsDeleted(); + + const deletedSnapshot = commodity.toSnapshot(); + + let thrownError: unknown; + + try { + commodity.markAsDeleted(); + } catch (error) { + thrownError = error; + } + + expect(thrownError).toMatchObject({ + code: 'DELETED_ENTITY_OPERATION', + context: { + entityType: Commodity.entityType, + operation: 'delete', + }, + }); + + expect(commodity.toSnapshot()).toEqual(deletedSnapshot); + }); + + it('should only mark commodity as deleted and update timestamp during soft deletion', () => { + vi.useFakeTimers(); + + const timestampBeforeDeletionValue = '2025-01-01T00:00:00.000Z'; + const timestampDuringDeletionValue = '2025-01-01T00:00:01.000Z'; + + vi.setSystemTime(new Date(timestampBeforeDeletionValue)); + + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + const commoditySnapshotBeforeDeletion = commodity.toSnapshot(); + + expect(commodity.isDeleted()).toBe(false); + + vi.setSystemTime(new Date(timestampDuringDeletionValue)); + + commodity.markAsDeleted(); + + const commoditySnapshotAfterDeletion = commodity.toSnapshot(); + + expect(commodity.isDeleted()).toBe(true); + + expect(commoditySnapshotAfterDeletion).toEqual({ + ...commoditySnapshotBeforeDeletion, + isTombstone: true, + updatedAt: commoditySnapshotAfterDeletion.updatedAt, + }); + + expect(commoditySnapshotAfterDeletion.updatedAt).not.toBe( + commoditySnapshotBeforeDeletion.updatedAt, + ); + }); + }); + + describe('update method', () => { + it('should update commodity name, code, and symbol and touch the timestamp', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z')); + + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + const before = commodity.toSnapshot(); + + const newName = Name.create('Updated Commodity Name'); + const newCode = CommodityCode.create('updatedCode'); + const newSymbol = 'UCMD'; + + vi.setSystemTime(new Date('2025-01-01T00:00:01.000Z')); + + commodity.update({ + code: newCode.valueOf(), + name: newName.valueOf(), + symbol: newSymbol, + }); + + const updatedSnapshot = commodity.toSnapshot(); + + expect(updatedSnapshot.name).toBe(newName.valueOf()); + expect(updatedSnapshot.symbol).toBe(newSymbol); + expect(updatedSnapshot.code).toBe(newCode.valueOf()); + expect(updatedSnapshot.updatedAt).not.toBe(before.createdAt); + }); + }); + + it('should throw error when updating commodity with invalid symbol', () => { + const commodity = Commodity.create( + user, + commodityName, + commodityCode, + commodityPrecision, + commoditySymbol, + ); + + expect(() => { + commodity.update({ + symbol: 'INVALID_SYMBOL', + }); + }).toThrowError(); + }); + + it('should not update timestamp for empty update', () => { + const commodity = Commodity.create(user, commodityName, commodityCode); + const snapshot = commodity.toSnapshot(); + + commodity.update({}); + + expect(commodity.toSnapshot()).toEqual(snapshot); + }); + }); +}); diff --git a/apps/backend/src/domain/commodities/helpers.ts b/apps/backend/src/domain/commodities/helpers.ts new file mode 100644 index 00000000..211880aa --- /dev/null +++ b/apps/backend/src/domain/commodities/helpers.ts @@ -0,0 +1,34 @@ +import { + CommoditySymbolString, + CommodityPrecisionNumber, +} from '@ledgerly/shared/types'; +import { + commoditySymbol, + commodityPrecision, +} from '@ledgerly/shared/validation'; + +import { parseValueObject } from '../domain-core'; +import { + InvalidCommoditySymbolError, + InvalidCommodityPrecisionError, +} from '../domain.errors'; + +export const parseCommoditySymbol = ( + symbol: string | null, +): CommoditySymbolString | null => { + return parseValueObject(symbol, commoditySymbol, (cause, invalidValue) => { + throw new InvalidCommoditySymbolError(invalidValue, cause); + }); +}; + +export const parseCommodityPrecision = ( + precision: number, +): CommodityPrecisionNumber => { + return parseValueObject( + precision, + commodityPrecision, + (cause, invalidValue) => { + throw new InvalidCommodityPrecisionError(invalidValue, cause); + }, + ); +}; diff --git a/apps/backend/src/domain/commodities/index.ts b/apps/backend/src/domain/commodities/index.ts new file mode 100644 index 00000000..adfe6c1b --- /dev/null +++ b/apps/backend/src/domain/commodities/index.ts @@ -0,0 +1 @@ +export { Commodity } from './commodity.entity'; diff --git a/apps/backend/src/domain/commodities/types.ts b/apps/backend/src/domain/commodities/types.ts new file mode 100644 index 00000000..1f8ddb04 --- /dev/null +++ b/apps/backend/src/domain/commodities/types.ts @@ -0,0 +1,23 @@ +import { + CommodityCodeString, + IsoDatetimeString, + UUID, +} from '@ledgerly/shared/types'; + +export type CommoditySnapshot = { + createdAt: IsoDatetimeString; + id: UUID; + isTombstone: boolean; + code: CommodityCodeString; + symbol: string | null; + updatedAt: IsoDatetimeString; + userId: UUID; + name: string; + precision: number; +}; + +export type CommodityUpdateProps = Partial<{ + code: CommodityCodeString; + symbol: string | null; + name: string; +}>; diff --git a/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts new file mode 100644 index 00000000..3d558e0d --- /dev/null +++ b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts @@ -0,0 +1,89 @@ +import { InvalidCommodityCodeError } from 'src/domain'; +import { describe, expect, it } from 'vitest'; + +import { CommodityCode } from './CommodityCode'; + +describe('CommodityCode Value Object', () => { + describe('create method', () => { + it('should create valid commodity code', () => { + const commodityCode = CommodityCode.create('CMD'); + + expect(commodityCode.valueOf()).toBe('CMD'); + }); + + it('should normalize commodity code to uppercase', () => { + const commodityCode = CommodityCode.create('cmd'); + + expect(commodityCode.valueOf()).toBe('CMD'); + }); + + it('should trim whitespace', () => { + const commodityCode = CommodityCode.create(' CMD '); + + expect(commodityCode.valueOf()).toBe('CMD'); + }); + + it('should throw error for invalid commodity codes', () => { + const invalidCodes = [ + '', + ' ', + ' ', + 'US', + '123', + '1USD', + 'USD EUR', + ' USD EUR ', + 'USD!', + 'USD$', + 'USD.', + 'USD,', + 'USD/', + 'USD\\', + 'USD@', + 'USD#', + '_USD', + '-USD', + 'РУБ', + '€', + 'ドル', + 'QWERTYUIOPASDFGHJ', + ]; + + invalidCodes.forEach((invalidCode) => { + expect(() => CommodityCode.create(invalidCode)).toThrow( + InvalidCommodityCodeError, + ); + }); + }); + + it('should accept valid commodity codes', () => { + const validCodes = [ + 'CMD', + 'USD', + 'EUR', + 'JPY', + 'GBP', + 'AUD', + 'CAD', + 'CHF', + 'CNY', + 'SEK', + 'RUB', + 'BTC', + 'ETH', + 'XAU', + 'BONUS', + 'WORKHOURS', + 'USD1', + 'ABC123', + 'usd', + ' UsD ', + 'QWERTYUIOPASDFGH', + ]; + + validCodes.forEach((validCode) => { + expect(() => CommodityCode.create(validCode)).not.toThrow(); + }); + }); + }); +}); diff --git a/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts new file mode 100644 index 00000000..332b3cdc --- /dev/null +++ b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts @@ -0,0 +1,34 @@ +import { CommodityCodeString } from '@ledgerly/shared/types'; +import { commodityCode } from '@ledgerly/shared/validation'; +import { InvalidCommodityCodeError } from 'src/domain'; + +import { parseValueObject } from './parseValueObject'; + +const parseCommodityCode = (code: string): CommodityCodeString => { + return parseValueObject(code, commodityCode, (cause, invalidValue) => { + throw new InvalidCommodityCodeError(invalidValue, cause); + }); +}; + +export class CommodityCode { + private constructor(private readonly code: CommodityCodeString) { + Object.freeze(this); + } + + static create(code: string): CommodityCode { + const normalizedCode = parseCommodityCode(code); + return new CommodityCode(normalizedCode); + } + + static restore(code: string): CommodityCode { + return new CommodityCode(parseCommodityCode(code)); + } + + equals(other: CommodityCode): boolean { + return this.code === other.code; + } + + valueOf(): CommodityCodeString { + return this.code; + } +} diff --git a/apps/backend/src/domain/domain-core/value-objects/index.ts b/apps/backend/src/domain/domain-core/value-objects/index.ts index 039c697e..278baf2d 100644 --- a/apps/backend/src/domain/domain-core/value-objects/index.ts +++ b/apps/backend/src/domain/domain-core/value-objects/index.ts @@ -9,3 +9,4 @@ export { Password } from './Password'; export { DateValue } from './DateValue'; export { Version } from './Version'; export { parseValueObject } from './parseValueObject'; +export { CommodityCode } from './CommodityCode'; diff --git a/apps/backend/src/domain/domain.errors.ts b/apps/backend/src/domain/domain.errors.ts index 78e6de93..73f36d15 100644 --- a/apps/backend/src/domain/domain.errors.ts +++ b/apps/backend/src/domain/domain.errors.ts @@ -75,6 +75,57 @@ export class InvalidAmountError extends CodedDomainError<'INVALID_AMOUNT'> { } } +export class InvalidCommodityCodeError extends CodedDomainError<'INVALID_COMMODITY_CODE'> { + constructor( + public readonly value: unknown, + cause?: Error, + ) { + super( + 'commodity code is invalid', + apiErrorCodes.invalidCommodityCode, + { + reason: 'INVALID_FORMAT', + received: String(value), + }, + cause, + ); + } +} + +export class InvalidCommodityPrecisionError extends CodedDomainError<'INVALID_COMMODITY_PRECISION'> { + constructor( + public readonly value: unknown, + cause?: Error, + ) { + super( + 'commodity precision is invalid', + apiErrorCodes.invalidCommodityPrecision, + { + reason: 'INVALID_FORMAT', + received: String(value), + }, + cause, + ); + } +} + +export class InvalidCommoditySymbolError extends CodedDomainError<'INVALID_COMMODITY_SYMBOL'> { + constructor( + public readonly value: unknown, + cause?: Error, + ) { + super( + 'commodity symbol is invalid', + apiErrorCodes.invalidCommoditySymbol, + { + reason: 'INVALID_FORMAT', + received: String(value), + }, + cause, + ); + } +} + export class InvalidDateError extends CodedDomainError<'INVALID_DATE'> { constructor(cause?: Error) { super( diff --git a/apps/backend/src/domain/index.ts b/apps/backend/src/domain/index.ts index a8666551..c6be0b0c 100644 --- a/apps/backend/src/domain/index.ts +++ b/apps/backend/src/domain/index.ts @@ -2,5 +2,6 @@ export { User } from './users/user.entity'; export { Account, AccountType } from './accounts/'; export { Transaction } from './transactions/'; export { Operation } from './operations'; +export { Commodity } from './commodities'; export * from './domain.errors'; diff --git a/apps/backend/src/presentation/http/error-handler.ts b/apps/backend/src/presentation/http/error-handler.ts index 608a14dd..a4421476 100644 --- a/apps/backend/src/presentation/http/error-handler.ts +++ b/apps/backend/src/presentation/http/error-handler.ts @@ -26,6 +26,9 @@ const statusByErrorCode = { [apiErrorCodes.internalServerError]: 500, [apiErrorCodes.invalidAccountType]: 400, [apiErrorCodes.invalidAmount]: 400, + [apiErrorCodes.invalidCommodityCode]: 400, + [apiErrorCodes.invalidCommodityPrecision]: 400, + [apiErrorCodes.invalidCommoditySymbol]: 400, [apiErrorCodes.invalidDate]: 400, [apiErrorCodes.invalidEmail]: 400, [apiErrorCodes.invalidIdentifier]: 400, diff --git a/packages/shared/src/types/error-contract.ts b/packages/shared/src/types/error-contract.ts index 81189bae..f0db0b07 100644 --- a/packages/shared/src/types/error-contract.ts +++ b/packages/shared/src/types/error-contract.ts @@ -14,6 +14,9 @@ export const apiErrorCodes = { internalServerError: "INTERNAL_SERVER_ERROR", invalidAccountType: "INVALID_ACCOUNT_TYPE", invalidAmount: "INVALID_AMOUNT", + invalidCommodityCode: "INVALID_COMMODITY_CODE", + invalidCommodityPrecision: "INVALID_COMMODITY_PRECISION", + invalidCommoditySymbol: "INVALID_COMMODITY_SYMBOL", invalidDate: "INVALID_DATE", invalidEmail: "INVALID_EMAIL", invalidIdentifier: "INVALID_IDENTIFIER", @@ -91,6 +94,21 @@ export type ErrorContextByCode = { reason: "NOT_INTEGER_MINOR_UNITS"; received: string; }; + INVALID_COMMODITY_CODE: { + field?: string; + reason: "INVALID_FORMAT"; + received: string; + }; + INVALID_COMMODITY_PRECISION: { + field?: string; + reason: "INVALID_FORMAT"; + received: string; + }; + INVALID_COMMODITY_SYMBOL: { + field?: string; + reason: "INVALID_FORMAT"; + received: string; + }; INVALID_DATE: { reason: "INVALID_FORMAT"; }; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index ffd4afc4..0408bdad 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -7,6 +7,9 @@ import { sha256String, uuid, amountString, + commodityCode, + commodityPrecision, + commoditySymbol, } from "../validation/baseValidations"; export type CurrencyCode = z.infer; @@ -18,3 +21,7 @@ export type Sha256String = z.infer; export type UUID = z.infer; export type AmountString = z.infer; + +export type CommodityCodeString = z.infer; +export type CommodityPrecisionNumber = z.infer; +export type CommoditySymbolString = z.infer; diff --git a/packages/shared/src/validation/baseValidations.ts b/packages/shared/src/validation/baseValidations.ts index 5b53f1ec..487303c8 100644 --- a/packages/shared/src/validation/baseValidations.ts +++ b/packages/shared/src/validation/baseValidations.ts @@ -80,3 +80,33 @@ export const getTransactionsQuerySchema = z }); } }); + +const COMMODITY_CODE_MIN_LENGTH = 3; +const COMMODITY_CODE_MAX_LENGTH = 16; + +export const commodityCode = z + .string() + .trim() + .toUpperCase() + .min( + COMMODITY_CODE_MIN_LENGTH, + `Commodity code must be at least ${COMMODITY_CODE_MIN_LENGTH} character long`, + ) + .max( + COMMODITY_CODE_MAX_LENGTH, + `Commodity code must be at most ${COMMODITY_CODE_MAX_LENGTH} characters long`, + ) + .regex( + /^[A-Z][A-Z0-9]*$/, + "Commodity code must start with a letter and contain only letters and digits", + ) + .brand<"CommodityCode">(); + +export const commoditySymbol = z + .string() + .trim() + .max(12) + .transform((value) => value || null) + .nullable(); + +export const commodityPrecision = z.number().int().min(0).max(18); From 4b8b6f5987f4247304ee2a30b20abd56caaae352 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Wed, 15 Jul 2026 17:54:50 +0300 Subject: [PATCH 03/30] LED-109 refactor(commodities): update commodity domain types and validations --- apps/backend/src/domain/commodities/commodity.entity.ts | 7 +++++-- apps/backend/src/domain/commodities/commodity.test.ts | 4 ++-- apps/backend/src/domain/commodities/types.ts | 8 +++++--- .../domain-core/value-objects/CommodityCode.test.ts | 2 +- .../domain/domain-core/value-objects/CommodityCode.ts | 2 +- packages/shared/src/validation/baseValidations.ts | 9 ++------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/domain/commodities/commodity.entity.ts b/apps/backend/src/domain/commodities/commodity.entity.ts index a3c12473..cc300d1f 100644 --- a/apps/backend/src/domain/commodities/commodity.entity.ts +++ b/apps/backend/src/domain/commodities/commodity.entity.ts @@ -1,4 +1,7 @@ -import { CommodityPrecisionNumber } from '@ledgerly/shared/types'; +import { + CommodityPrecisionNumber, + CommoditySymbolString, +} from '@ledgerly/shared/types'; import { CommodityCode, @@ -24,7 +27,7 @@ export class Commodity { private softDelete: SoftDelete, private readonly ownership: ParentChildRelation, private code: CommodityCode, - public symbol: string | null, + public symbol: CommoditySymbolString | null, public name: Name, private precision: CommodityPrecisionNumber, ) {} diff --git a/apps/backend/src/domain/commodities/commodity.test.ts b/apps/backend/src/domain/commodities/commodity.test.ts index d11ffbfd..8654c82d 100644 --- a/apps/backend/src/domain/commodities/commodity.test.ts +++ b/apps/backend/src/domain/commodities/commodity.test.ts @@ -169,7 +169,7 @@ describe('Commodity Domain Entity', () => { expect(() => { Commodity.restore(snapshot); - }).toThrowError(); + }).toThrowError(InvalidCommoditySymbolError); }); }); @@ -313,7 +313,7 @@ describe('Commodity Domain Entity', () => { expect(updatedSnapshot.name).toBe(newName.valueOf()); expect(updatedSnapshot.symbol).toBe(newSymbol); expect(updatedSnapshot.code).toBe(newCode.valueOf()); - expect(updatedSnapshot.updatedAt).not.toBe(before.createdAt); + expect(updatedSnapshot.updatedAt).not.toBe(before.updatedAt); }); }); diff --git a/apps/backend/src/domain/commodities/types.ts b/apps/backend/src/domain/commodities/types.ts index 1f8ddb04..1af354a1 100644 --- a/apps/backend/src/domain/commodities/types.ts +++ b/apps/backend/src/domain/commodities/types.ts @@ -1,7 +1,9 @@ import { CommodityCodeString, + CommoditySymbolString, IsoDatetimeString, UUID, + CommodityPrecisionNumber, } from '@ledgerly/shared/types'; export type CommoditySnapshot = { @@ -9,15 +11,15 @@ export type CommoditySnapshot = { id: UUID; isTombstone: boolean; code: CommodityCodeString; - symbol: string | null; + symbol: CommoditySymbolString | null; updatedAt: IsoDatetimeString; userId: UUID; name: string; - precision: number; + precision: CommodityPrecisionNumber; }; export type CommodityUpdateProps = Partial<{ - code: CommodityCodeString; + code: string; symbol: string | null; name: string; }>; diff --git a/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts index 3d558e0d..2c0444a3 100644 --- a/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts +++ b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.test.ts @@ -1,4 +1,4 @@ -import { InvalidCommodityCodeError } from 'src/domain'; +import { InvalidCommodityCodeError } from 'src/domain/domain.errors'; import { describe, expect, it } from 'vitest'; import { CommodityCode } from './CommodityCode'; diff --git a/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts index 332b3cdc..fcdd05af 100644 --- a/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts +++ b/apps/backend/src/domain/domain-core/value-objects/CommodityCode.ts @@ -1,6 +1,6 @@ import { CommodityCodeString } from '@ledgerly/shared/types'; import { commodityCode } from '@ledgerly/shared/validation'; -import { InvalidCommodityCodeError } from 'src/domain'; +import { InvalidCommodityCodeError } from 'src/domain/domain.errors'; import { parseValueObject } from './parseValueObject'; diff --git a/packages/shared/src/validation/baseValidations.ts b/packages/shared/src/validation/baseValidations.ts index 487303c8..62fc00cf 100644 --- a/packages/shared/src/validation/baseValidations.ts +++ b/packages/shared/src/validation/baseValidations.ts @@ -90,7 +90,7 @@ export const commodityCode = z .toUpperCase() .min( COMMODITY_CODE_MIN_LENGTH, - `Commodity code must be at least ${COMMODITY_CODE_MIN_LENGTH} character long`, + `Commodity code must be at least ${COMMODITY_CODE_MIN_LENGTH} characters long`, ) .max( COMMODITY_CODE_MAX_LENGTH, @@ -102,11 +102,6 @@ export const commodityCode = z ) .brand<"CommodityCode">(); -export const commoditySymbol = z - .string() - .trim() - .max(12) - .transform((value) => value || null) - .nullable(); +export const commoditySymbol = z.string().trim().max(12).nullable(); export const commodityPrecision = z.number().int().min(0).max(18); From 736858eb8778f05dafb393077e848c291ce5c893 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 16 Jul 2026 11:37:06 +0300 Subject: [PATCH 04/30] LED-66 feat(commodities): add commodities schema and update exports --- apps/backend/src/db/schema.ts | 1 + apps/backend/src/db/schemas/commodities.ts | 39 ++++++++++++++++++++++ apps/backend/src/db/schemas/index.ts | 1 + 3 files changed, 41 insertions(+) create mode 100644 apps/backend/src/db/schemas/commodities.ts diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts index 70649edc..a8bac057 100644 --- a/apps/backend/src/db/schema.ts +++ b/apps/backend/src/db/schema.ts @@ -4,3 +4,4 @@ export * from './schemas/currencies'; export * from './schemas/operations'; export * from './schemas/users'; export * from './schemas/settings'; +export * from './schemas/commodities'; diff --git a/apps/backend/src/db/schemas/commodities.ts b/apps/backend/src/db/schemas/commodities.ts new file mode 100644 index 00000000..d1026a7c --- /dev/null +++ b/apps/backend/src/db/schemas/commodities.ts @@ -0,0 +1,39 @@ +import { UUID } from '@ledgerly/shared/types'; +import { InferInsertModel, InferSelectModel } from 'drizzle-orm'; +import { + integer, + sqliteTable, + text, + uniqueIndex, +} from 'drizzle-orm/sqlite-core'; + +import { createdAt, description, updatedAt, id, isTombstone } from './common'; +import { usersTable } from './users'; + +export const commoditiesTable = sqliteTable( + 'commodities', + { + code: text('code').notNull(), + createdAt, + description, + id, + isTombstone, + name: text('name').notNull(), + precision: integer('precision').notNull(), + symbol: text('symbol'), + updatedAt, + userId: text('user_id') + .notNull() + .references(() => usersTable.id, { onDelete: 'cascade' }) + .$type(), + }, + (table) => [ + uniqueIndex('commodities_user_id_code_unique_idx').on( + table.userId, + table.code, + ), + ], +); + +export type CommodityDbRow = InferSelectModel; +export type CommodityDbInsert = InferInsertModel; diff --git a/apps/backend/src/db/schemas/index.ts b/apps/backend/src/db/schemas/index.ts index 8521abfd..de403604 100644 --- a/apps/backend/src/db/schemas/index.ts +++ b/apps/backend/src/db/schemas/index.ts @@ -4,3 +4,4 @@ export { operationsTable, operationsRelations } from './operations'; export { transactionsTable, transactionsRelations } from './transactions'; export { settingsTable } from './settings'; export { usersTable } from './users'; +export { commoditiesTable } from './commodities'; From c0ed759d5d8a9ccf24685796cfc9a381ed68b993 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Fri, 17 Jul 2026 12:45:53 +0300 Subject: [PATCH 05/30] LED-66 feat(accounts): add commodity support to account creation and management --- .../drizzle/0001_small_mister_fear.sql | 15 + apps/backend/drizzle/meta/0001_snapshot.json | 660 ++++++++++++++++++ apps/backend/drizzle/meta/_journal.json | 7 + .../CommodityRepository.interface.ts | 6 + .../src/application/interfaces/index.ts | 1 + .../mappers/account.mapper.test.ts | 2 + .../src/application/mappers/account.mapper.ts | 1 + .../__tests__/account.factory.test.ts | 72 -- .../application/services/account.factory.ts | 29 - .../backend/src/application/services/index.ts | 1 - .../accounts/__tests__/createAccount.test.ts | 96 ++- .../accounts/__tests__/deleteAccount.test.ts | 1 + .../accounts/__tests__/getAccountById.test.ts | 4 +- .../accounts/__tests__/getAllAccounts.test.ts | 4 +- .../accounts/__tests__/updateAccount.test.ts | 4 +- .../usecases/accounts/createAccount.ts | 43 +- apps/backend/src/db/createTestUser.ts | 86 +-- apps/backend/src/db/schemas/accounts.ts | 9 +- apps/backend/src/db/schemas/commodities.ts | 16 +- apps/backend/src/db/test-db.ts | 92 ++- .../src/db/test-utils/testEntityBuilder.ts | 61 +- apps/backend/src/di/container.ts | 16 +- apps/backend/src/di/types.ts | 8 +- .../src/domain/accounts/account.entity.ts | 46 +- .../src/domain/accounts/account.test.ts | 39 +- apps/backend/src/domain/accounts/types.ts | 1 + apps/backend/src/domain/commodities/index.ts | 1 + .../account-persistence.mapper.test.ts | 3 + .../db/accounts/account-persistence.mapper.ts | 2 + .../db/accounts/account.repository.test.ts | 129 ++-- .../db/accounts/account.repository.ts | 19 + .../commodity-persistence.mapper.test.ts | 61 ++ .../commodity-persistence.mapper.ts | 37 + .../commodities/commodity.repository.test.ts | 79 +++ .../db/commodities/commodity.repository.ts | 34 + .../infrastructure/db/commodities/index.ts | 2 + apps/backend/src/infrastructure/db/index.ts | 1 + .../operations/operation.repository.test.ts | 11 + .../transaction-query.repository.test.ts | 28 +- .../transaction.repository.test.ts | 11 + .../http/accounts/account.controller.test.ts | 1 + .../http/accounts/account.integration.test.ts | 35 +- .../transaction.integration.test.ts | 48 +- packages/shared/src/types/account.ts | 2 + packages/shared/src/validation/accounts.ts | 1 + 45 files changed, 1494 insertions(+), 331 deletions(-) create mode 100644 apps/backend/drizzle/0001_small_mister_fear.sql create mode 100644 apps/backend/drizzle/meta/0001_snapshot.json create mode 100644 apps/backend/src/application/interfaces/CommodityRepository.interface.ts delete mode 100644 apps/backend/src/application/services/__tests__/account.factory.test.ts delete mode 100644 apps/backend/src/application/services/account.factory.ts create mode 100644 apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.test.ts create mode 100644 apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.ts create mode 100644 apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts create mode 100644 apps/backend/src/infrastructure/db/commodities/commodity.repository.ts create mode 100644 apps/backend/src/infrastructure/db/commodities/index.ts diff --git a/apps/backend/drizzle/0001_small_mister_fear.sql b/apps/backend/drizzle/0001_small_mister_fear.sql new file mode 100644 index 00000000..1baf91b0 --- /dev/null +++ b/apps/backend/drizzle/0001_small_mister_fear.sql @@ -0,0 +1,15 @@ +CREATE TABLE `commodities` ( + `code` text NOT NULL, + `created_at` text NOT NULL, + `id` text PRIMARY KEY NOT NULL, + `is_tombstone` integer NOT NULL, + `name` text NOT NULL, + `precision` integer NOT NULL, + `symbol` text, + `updated_at` text NOT NULL, + `user_id` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `commodities_user_id_code_unique_idx` ON `commodities` (`user_id`,`code`);--> statement-breakpoint +ALTER TABLE `accounts` ADD `commodity_id` text NOT NULL REFERENCES commodities(id); \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..48a15077 --- /dev/null +++ b/apps/backend/drizzle/meta/0001_snapshot.json @@ -0,0 +1,660 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "694ae242-fadb-4fa3-ad15-1548710dd82a", + "prevId": "b3626041-a73e-4294-b580-3c53764e0f43", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "commodity_id": { + "name": "commodity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "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", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initial_balance": { + "name": "initial_balance", + "type": "text", + "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", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_name_unique_idx": { + "name": "user_id_name_unique_idx", + "columns": [ + "user_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "accounts_commodity_id_commodities_id_fk": { + "name": "accounts_commodity_id_commodities_id_fk", + "tableFrom": "accounts", + "tableTo": "commodities", + "columnsFrom": [ + "commodity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transactions": { + "name": "transactions", + "columns": { + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "posting_date": { + "name": "posting_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transaction_date": { + "name": "transaction_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_transactions_user_date": { + "name": "idx_transactions_user_date", + "columns": [ + "user_id", + "transaction_date" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "currencies": { + "name": "currencies", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "operations": { + "name": "operations", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_operations_transaction": { + "name": "idx_operations_transaction", + "columns": [ + "transaction_id" + ], + "isUnique": false + }, + "idx_operations_account": { + "name": "idx_operations_account", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "idx_operations_user": { + "name": "idx_operations_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "operations_account_id_accounts_id_fk": { + "name": "operations_account_id_accounts_id_fk", + "tableFrom": "operations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "operations_transaction_id_transactions_id_fk": { + "name": "operations_transaction_id_transactions_id_fk", + "tableFrom": "operations", + "tableTo": "transactions", + "columnsFrom": [ + "transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "operations_user_id_users_id_fk": { + "name": "operations_user_id_users_id_fk", + "tableFrom": "operations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "base_currency": { + "name": "base_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RUB'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_base_currency_currencies_code_fk": { + "name": "settings_base_currency_currencies_code_fk", + "tableFrom": "settings", + "tableTo": "currencies", + "columnsFrom": [ + "base_currency" + ], + "columnsTo": [ + "code" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "settings_user_id_users_id_fk": { + "name": "settings_user_id_users_id_fk", + "tableFrom": "settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "commodities": { + "name": "commodities", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "precision": { + "name": "precision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "commodities_user_id_code_unique_idx": { + "name": "commodities_user_id_code_unique_idx", + "columns": [ + "user_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "commodities_user_id_users_id_fk": { + "name": "commodities_user_id_users_id_fk", + "tableFrom": "commodities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index f3fff04a..330ef8b0 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1776091943739, "tag": "0000_keen_gertrude_yorkes", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1784193820034, + "tag": "0001_small_mister_fear", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts new file mode 100644 index 00000000..c5dae07a --- /dev/null +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -0,0 +1,6 @@ +import { UUID } from '@ledgerly/shared/types'; +import { CommoditySnapshot } from 'src/domain/commodities'; + +export type CommodityRepositoryInterface = { + getById(userId: string, commodityId: UUID): Promise; +}; diff --git a/apps/backend/src/application/interfaces/index.ts b/apps/backend/src/application/interfaces/index.ts index 0853ca8a..62dd85a8 100644 --- a/apps/backend/src/application/interfaces/index.ts +++ b/apps/backend/src/application/interfaces/index.ts @@ -17,3 +17,4 @@ export type { TransactionContext, } from './TransactionManager.interface'; export type { OperationRepositoryInterface } from './OperationRepository.interface'; +export type { CommodityRepositoryInterface } from './CommodityRepository.interface'; diff --git a/apps/backend/src/application/mappers/account.mapper.test.ts b/apps/backend/src/application/mappers/account.mapper.test.ts index dff6a06b..b572a570 100644 --- a/apps/backend/src/application/mappers/account.mapper.test.ts +++ b/apps/backend/src/application/mappers/account.mapper.test.ts @@ -7,6 +7,7 @@ import { AccountMapper } from './account.mapper'; describe('AccountMapper', () => { const snapshot: AccountSnapshot = { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency: Currency.create('USD').valueOf(), currentClearedBalanceLocal: Amount.create('2500').valueOf(), @@ -23,6 +24,7 @@ describe('AccountMapper', () => { it('maps an account snapshot to a response DTO', () => { expect(AccountMapper.toResponseDTOFromSnapshot(snapshot)).toEqual({ + commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, currency: snapshot.currency, currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal, diff --git a/apps/backend/src/application/mappers/account.mapper.ts b/apps/backend/src/application/mappers/account.mapper.ts index 162c1191..7e5aa206 100644 --- a/apps/backend/src/application/mappers/account.mapper.ts +++ b/apps/backend/src/application/mappers/account.mapper.ts @@ -6,6 +6,7 @@ export class AccountMapper { snapshot: AccountSnapshot, ): AccountResponseDTO { return { + commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, currency: snapshot.currency, currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal, diff --git a/apps/backend/src/application/services/__tests__/account.factory.test.ts b/apps/backend/src/application/services/__tests__/account.factory.test.ts deleted file mode 100644 index f705e379..00000000 --- a/apps/backend/src/application/services/__tests__/account.factory.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; -import type { AccountRepositoryInterface } from 'src/application/interfaces'; -import { createUser } from 'src/db/createTestUser'; -import { User } from 'src/domain'; -import { Account, AccountType } from 'src/domain/'; -import { Amount, Currency, Name } from 'src/domain/domain-core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { AccountFactory } from '../account.factory'; - -describe('CreateAccountUseCase', () => { - let user: User; - - const mockAccountRepository = { - create: vi.fn(), - findSystemAccount: vi.fn(), - }; - - const accountFactory = new AccountFactory( - mockAccountRepository as unknown as AccountRepositoryInterface, - ); - - const accountName = 'Test Account'; - const description = 'Test account description'; - const initialBalance = Amount.create('1000').valueOf(); - const currency = 'USD' as CurrencyCode; - const accountType = 'asset'; - - beforeEach(async () => { - user = await createUser(); - }); - - describe('createAccount', () => { - it('should create an account with correct properties', async () => { - const expectedAccount = Account.create( - user, - Name.create(accountName), - description, - Amount.create(initialBalance), - Currency.create(currency), - AccountType.create(accountType), - ); - - mockAccountRepository.create.mockResolvedValue( - expectedAccount.toSnapshot(), - ); - - const account = await accountFactory.createAccount(user, { - currency, - description, - initialBalance, - name: accountName, - type: accountType, - }); - - expect(account).toBeInstanceOf(Account); - - const accountSnapshot = account.toSnapshot(); - - expect(accountSnapshot.name).toBe(accountName); - expect(accountSnapshot.description).toBe(description); - expect(accountSnapshot.initialBalance).toBe(initialBalance); - expect(accountSnapshot.currency).toBe(currency); - expect(accountSnapshot.type).toBe(accountType); - expect(accountSnapshot.isSystem).toBe(false); - - expect(mockAccountRepository.create).toHaveBeenCalledWith( - accountSnapshot, - ); - }); - }); -}); diff --git a/apps/backend/src/application/services/account.factory.ts b/apps/backend/src/application/services/account.factory.ts deleted file mode 100644 index e75cc3a0..00000000 --- a/apps/backend/src/application/services/account.factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AccountCreateDTO } from '@ledgerly/shared/types'; -import { AccountType, Account } from 'src/domain/'; -import { Amount, Currency, Name } from 'src/domain/domain-core'; -import { User } from 'src/domain/users/user.entity'; - -import { AccountRepositoryInterface } from '../interfaces'; - -export class AccountFactory { - constructor( - protected readonly accountRepository: AccountRepositoryInterface, - ) {} - - async createAccount(user: User, data: AccountCreateDTO): Promise { - const { currency, description, initialBalance, name, type } = data; - - const account = Account.create( - user, - Name.create(name), - description, - Amount.create(initialBalance), - Currency.create(currency), - AccountType.create(type), - ); - - await this.accountRepository.create(account.toSnapshot()); - - return account; - } -} diff --git a/apps/backend/src/application/services/index.ts b/apps/backend/src/application/services/index.ts index 60f316b9..3847868e 100644 --- a/apps/backend/src/application/services/index.ts +++ b/apps/backend/src/application/services/index.ts @@ -1,2 +1 @@ -export * from './account.factory'; export * from './TransactionService'; diff --git a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts index facb8d7d..cfd31bff 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts @@ -1,9 +1,12 @@ import { CurrencyCode } from '@ledgerly/shared/types'; -import { AccountFactory } from 'src/application/services/account.factory'; +import { + AccountRepositoryInterface, + CommodityRepositoryInterface, + EntityNotFoundError, +} from 'src/application'; import { createUser } from 'src/db/createTestUser'; -import { AccountType } from 'src/domain'; -import { Account } from 'src/domain/accounts/account.entity'; -import { Amount, Currency, Name } from 'src/domain/domain-core'; +import { Commodity } from 'src/domain'; +import { Amount, CommodityCode, Name } from 'src/domain/domain-core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CreateAccountUseCase } from '../createAccount'; @@ -13,21 +16,32 @@ describe('CreateAccountUseCase', async () => { let createAccountUseCase: CreateAccountUseCase; - let accountFactory: { createAccount: ReturnType }; + const commodityRepository = { + getById: vi.fn(), + }; + const accountRepository = { + create: vi.fn(), + }; const name = 'Test Account'; const description = 'Test account description'; const initialBalance = Amount.create('1000').valueOf(); + const currentClearedBalanceLocal = Amount.create('0').valueOf(); const currency = 'USD' as CurrencyCode; + + const commodity = Commodity.create( + user, + Name.create('Test Commodity'), + CommodityCode.create('TEST'), + 2, + null, + ); const type = 'asset'; beforeEach(() => { - accountFactory = { - createAccount: vi.fn(), - }; - createAccountUseCase = new CreateAccountUseCase( - accountFactory as unknown as AccountFactory, + accountRepository as unknown as AccountRepositoryInterface, + commodityRepository as unknown as CommodityRepositoryInterface, ); }); @@ -35,19 +49,12 @@ describe('CreateAccountUseCase', async () => { it('should create a new account successfully', async () => { // Arrange - const mockedAccount = Account.create( - user, - Name.create(name), - description, - Amount.create(initialBalance), - Currency.create(currency), - AccountType.create(type), - ); - - accountFactory.createAccount.mockResolvedValue(mockedAccount); + const mockedCommoditySnapshot = commodity.toSnapshot(); + commodityRepository.getById.mockResolvedValue(mockedCommoditySnapshot); // Act const result = await createAccountUseCase.execute(user, { + commodityId: mockedCommoditySnapshot.id, currency: currency, description, initialBalance, @@ -55,12 +62,25 @@ describe('CreateAccountUseCase', async () => { type, }); - expect(accountFactory.createAccount).toHaveBeenCalledWith(user, { - currency: currency, + expect(commodityRepository.getById).toHaveBeenCalledWith( + user.getId().valueOf(), + mockedCommoditySnapshot.id, + ); + + expect(accountRepository.create).toHaveBeenCalledWith({ + commodityId: result.commodityId, + createdAt: result.createdAt, + currency, + currentClearedBalanceLocal, description, + id: result.id, initialBalance, + isSystem: false, + isTombstone: false, name, type, + updatedAt: result.updatedAt, + userId: result.userId, }); expect(result).toMatchObject({ @@ -73,5 +93,37 @@ describe('CreateAccountUseCase', async () => { userId: user.getId().valueOf(), }); }); + + it('should throw an error if the commodity does not belong to the user', async () => { + // Arrange + const anotherUser = await createUser(); + const anotherCommodity = Commodity.create( + anotherUser, + Name.create('Another Commodity'), + CommodityCode.create('ANOTHER'), + 2, + null, + ); + const mockedAnotherCommoditySnapshot = anotherCommodity.toSnapshot(); + + commodityRepository.getById.mockRejectedValue( + new EntityNotFoundError({ + entityId: mockedAnotherCommoditySnapshot.id, + entityType: 'commodity', + }), + ); + + // Act & Assert + await expect( + createAccountUseCase.execute(user, { + commodityId: mockedAnotherCommoditySnapshot.id, + currency: currency, + description, + initialBalance, + name, + type, + }), + ).rejects.toThrowError(EntityNotFoundError); + }); }); }); diff --git a/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts index 46f8aaca..3e9b3cde 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts @@ -39,6 +39,7 @@ describe('DeleteAccountUseCase', async () => { }; const mockAccountData: AccountSnapshot = { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency: currencyCode, currentClearedBalanceLocal: initialBalance, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts index 3afa62bb..ff05a204 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts @@ -11,6 +11,7 @@ import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; import { createUser } from 'src/db/createTestUser'; import { Account } from 'src/domain/accounts/account.entity'; +import { AccountSnapshot } from 'src/domain/accounts/types'; import { Amount, Timestamp } from 'src/domain/domain-core'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -44,7 +45,8 @@ describe('GetAccountByIdUseCase', async () => { name: 'Test User', }; - const mockSavedAccountData = { + const mockSavedAccountData: AccountSnapshot = { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency, currentClearedBalanceLocal: initialBalance, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts index 08321e04..ec601078 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts @@ -2,6 +2,7 @@ import { AccountTypeValue, CurrencyCode } from '@ledgerly/shared/types'; import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; import { createUser } from 'src/db/createTestUser'; +import { AccountSnapshot } from 'src/domain/accounts/types'; import { Amount, Timestamp } from 'src/domain/domain-core'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -26,7 +27,8 @@ describe('GetAllAccounts', async () => { const currency = 'USD' as CurrencyCode; const accountType = 'asset' as AccountTypeValue; - const mockSavedAccountData = { + const mockSavedAccountData: AccountSnapshot = { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency, currentClearedBalanceLocal: initialBalance, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts index 209671b5..1604e63c 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts @@ -8,6 +8,7 @@ import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; import { createUser } from 'src/db/createTestUser'; import { Account } from 'src/domain/accounts/account.entity'; +import { AccountSnapshot } from 'src/domain/accounts/types'; import { Amount, Timestamp } from 'src/domain/domain-core'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -35,7 +36,8 @@ describe('UpdateAccount', async () => { const currency = 'USD' as CurrencyCode; const accountType = 'asset' as AccountTypeValue; - const mockAccountData = { + const mockAccountData: AccountSnapshot = { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency, currentClearedBalanceLocal: initialBalance, diff --git a/apps/backend/src/application/usecases/accounts/createAccount.ts b/apps/backend/src/application/usecases/accounts/createAccount.ts index 560ac22a..8f58a0c8 100644 --- a/apps/backend/src/application/usecases/accounts/createAccount.ts +++ b/apps/backend/src/application/usecases/accounts/createAccount.ts @@ -1,25 +1,46 @@ import { AccountCreateDTO, AccountResponseDTO } from '@ledgerly/shared/types'; +import type { + AccountRepositoryInterface, + CommodityRepositoryInterface, +} from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; -import { AccountFactory } from 'src/application/services'; +import { AccountType, Account, Commodity } from 'src/domain/'; +import { Amount, Currency, Name } from 'src/domain/domain-core'; import { User } from 'src/domain/users/user.entity'; export class CreateAccountUseCase { - constructor(protected readonly accountFactory: AccountFactory) {} - + constructor( + protected readonly accountRepository: AccountRepositoryInterface, + protected readonly commodityRepository: CommodityRepositoryInterface, + ) {} async execute( user: User, data: AccountCreateDTO, ): Promise { - const { currency, description, initialBalance, name, type } = data; + const { commodityId, currency, description, initialBalance, name, type } = + data; + + const commoditySnapshot = await this.commodityRepository.getById( + user.getId().valueOf(), + commodityId, + ); + + const commodity = Commodity.restore(commoditySnapshot); - const account = await this.accountFactory.createAccount(user, { - currency, + const account = Account.create( + user, + commodity, + Name.create(name), description, - initialBalance, - name, - type, - }); + Amount.create(initialBalance), + Currency.create(currency), + AccountType.create(type), + ); + + const accountSnapshot = account.toSnapshot(); + + await this.accountRepository.create(accountSnapshot); - return AccountMapper.toResponseDTOFromSnapshot(account.toSnapshot()); + return AccountMapper.toResponseDTOFromSnapshot(accountSnapshot); } } diff --git a/apps/backend/src/db/createTestUser.ts b/apps/backend/src/db/createTestUser.ts index 829ff026..edda0a53 100644 --- a/apps/backend/src/db/createTestUser.ts +++ b/apps/backend/src/db/createTestUser.ts @@ -1,10 +1,11 @@ -import { Account, AccountType } from 'src/domain'; +import { Account, AccountType, Commodity } from 'src/domain'; import { Name, Email, Password, Amount, Currency, + CommodityCode, } from 'src/domain/domain-core'; import { User } from 'src/domain/users/user.entity'; @@ -28,12 +29,46 @@ export const createUser = async ( return User.create(userName, userEmail, userPassword); }; +export const createCommodity = ( + user: User, + params: { + name?: string; + code?: string; + precision?: number; + symbol?: string | null; + } = {}, +): Commodity => { + const { + code = 'TEST', + name = 'Test Commodity', + precision = 2, + symbol = null, + } = params; + + return Commodity.create( + user, + Name.create(name), + CommodityCode.create(code), + precision, + symbol, + ); +}; + export const createAccount = ( user: User, - params: { currency?: Currency; description?: string; name?: string } = {}, + + params: { + currency?: Currency; + description?: string; + name?: string; + commodity?: Commodity; + } = {}, ) => { + const commodity = params.commodity ?? createCommodity(user); + return Account.create( user, + commodity, Name.create(params.name ?? 'Test Account'), params.description ?? 'Account for testing', Amount.create('0'), @@ -41,50 +76,3 @@ export const createAccount = ( AccountType.create('asset'), ); }; - -// export const createTransaction = ( -// user: User, -// params: { -// description?: string; -// postingDate?: IsoDateString; -// transactionDate?: IsoDateString; -// } = {}, -// ) => { -// const { -// description = 'Test Transaction', -// postingDate = '2023-01-01' as IsoDateString, -// transactionDate = '2023-01-01' as IsoDateString, -// } = params; - -// // const postingDate = DateValue.restore(postingDate) - -// return Transaction.create( -// user, -// { -// description, -// entries: [], -// postingDate, -// transactionDate, -// }, -// {}, // EntryContext placeholder -// // DateValue.restore(transactionDate), -// ); -// }; - -// export const createEntry = ( -// user: User, -// transaction: Transaction, -// operations: Operation[], -// ) => { -// return Entry.create(user, transaction, 'Test entry', operations); -// }; - -// export const createOperation = ( -// user: User, -// account: Account, -// entry: Entry, -// amount: Amount, -// description: string, -// ) => { -// return Operation.create(user, account, entry, amount, description); -// }; diff --git a/apps/backend/src/db/schemas/accounts.ts b/apps/backend/src/db/schemas/accounts.ts index 3dc6ddf2..02119ae6 100644 --- a/apps/backend/src/db/schemas/accounts.ts +++ b/apps/backend/src/db/schemas/accounts.ts @@ -3,6 +3,7 @@ import { CurrencyCode, UUID } from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel } from 'drizzle-orm'; import { sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; +import { commoditiesTable } from './commodities'; import { createdAt, description, @@ -17,11 +18,11 @@ import { usersTable } from './users'; export const accountsTable = sqliteTable( 'accounts', { + commodityId: text('commodity_id') + .notNull() + .references(() => commoditiesTable.id) + .$type(), createdAt, - // Foreign key constraint to the currencies table has been removed. - // This allows invalid currency codes to be inserted, which may improve test performance, - // but creates a risk of data inconsistency in production. - // Consider implementing application-level validation for currency codes. currency: text('currency').notNull().$type(), currentClearedBalanceLocal: getAmountColumn( 'current_cleared_balance_local', diff --git a/apps/backend/src/db/schemas/commodities.ts b/apps/backend/src/db/schemas/commodities.ts index d1026a7c..4a919a6e 100644 --- a/apps/backend/src/db/schemas/commodities.ts +++ b/apps/backend/src/db/schemas/commodities.ts @@ -1,4 +1,9 @@ -import { UUID } from '@ledgerly/shared/types'; +import { + CommodityCodeString, + CommodityPrecisionNumber, + UUID, + CommoditySymbolString, +} from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel } from 'drizzle-orm'; import { integer, @@ -7,20 +12,19 @@ import { uniqueIndex, } from 'drizzle-orm/sqlite-core'; -import { createdAt, description, updatedAt, id, isTombstone } from './common'; +import { createdAt, updatedAt, id, isTombstone } from './common'; import { usersTable } from './users'; export const commoditiesTable = sqliteTable( 'commodities', { - code: text('code').notNull(), + code: text('code').notNull().$type(), createdAt, - description, id, isTombstone, name: text('name').notNull(), - precision: integer('precision').notNull(), - symbol: text('symbol'), + precision: integer('precision').notNull().$type(), + symbol: text('symbol').$type(), updatedAt, userId: text('user_id') .notNull() diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index cc3206e3..040c9cff 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -9,6 +9,9 @@ import { IsoDateString, AmountString, UUID, + CommodityCodeString, + CommodityPrecisionNumber, + CommoditySymbolString, } from '@ledgerly/shared/types'; import { isoDate, isoDatetime } from '@ledgerly/shared/validation'; import { createClient } from '@libsql/client'; @@ -16,7 +19,12 @@ import { eq, sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/libsql'; import { migrate } from 'drizzle-orm/libsql/migrator'; import { DataBase } from 'src/db'; -import { Amount, Currency, DateValue } from 'src/domain/domain-core'; +import { + Amount, + CommodityCode, + Currency, + DateValue, +} from 'src/domain/domain-core'; import { OperationSnapshot } from 'src/domain/operations/types'; import { TransactionSnapshot } from 'src/domain/transactions/types'; import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; @@ -33,6 +41,8 @@ import { AccountDbInsert, OperationDbInsert, OperationDbRow, + CommodityDbInsert, + commoditiesTable, } from './schema'; import * as schema from './schemas'; @@ -55,10 +65,13 @@ class Counter { return this.count; } - getNextName(suffix = ''): string { + getNextName({ + delimiter = '-', + suffix = '', + }: { suffix?: string; delimiter?: string } = {}): string { this.increment(); if (this.name) { - return `${this.name}-${this.count}${suffix ? `-${suffix}` : ''}`; + return `${this.name}${delimiter}${this.count}${suffix ? `${delimiter}${suffix}` : ''}`; } return this.count.toString(); @@ -94,6 +107,7 @@ export type TransactionSeed = { export class TestDB { db: DataBase; transactionCounter = new Counter('transaction'); + commodityCounter = new Counter('commodity'); operationCounter = new Counter('operation'); userCounter = new Counter('user'); private testDbFile?: string; @@ -408,8 +422,49 @@ export class TestDB { .get(); }; + createCommodity = async ( + userId: UUID, + params?: { + code?: CommodityCodeString; + symbol?: CommoditySymbolString; + name?: string; + precision?: CommodityPrecisionNumber; + }, + ) => { + const commodityData = { + code: + params?.code ?? + CommodityCode.create( + `COM${this.commodityCounter.getNextName({ delimiter: '' })}`, + ).valueOf(), + name: + params?.name ?? `Commodity ${this.transactionCounter.getNextName()}`, + precision: params?.precision ?? 2, + symbol: params?.symbol ?? `${this.commodityCounter.getNextName()}`, + userId, + }; + + const commodity = await this.db + .insert(schema.commoditiesTable) + .values({ + code: commodityData.code, + name: commodityData.name, + precision: commodityData.precision, + symbol: commodityData.symbol, + userId: commodityData.userId, + ...TestDB.createTimestamps, + ...TestDB.uuid, + isTombstone: false, + }) + .returning() + .get(); + + return commodity; + }; + createAccount = async ( userId: UUID, + commodityId: UUID, params?: { name?: string; currency?: CurrencyCode; @@ -433,6 +488,7 @@ export class TestDB { const account = await this.db .insert(accountsTable) .values({ + commodityId, currency: accountData.currency, currentClearedBalanceLocal: accountData.initialBalance ?? 0, description: accountData.description || '', @@ -451,6 +507,16 @@ export class TestDB { return account; }; + insertCommodity = async (commodityData: CommodityDbInsert) => { + const insertedCommodity = await this.db + .insert(commoditiesTable) + .values(commodityData) + .returning() + .get(); + + return insertedCommodity; + }; + insertAccount = async (accountData: AccountDbInsert) => { const account = await this.db .insert(accountsTable) @@ -539,17 +605,31 @@ export class TestDB { password: 'hashed_password', })); - const accountUSD1 = await this.createAccount(user.id, { + const usdCommodity = await this.createCommodity(user.id, { + code: CommodityCode.create('USD').valueOf(), + name: 'US Dollar', + precision: 2, + symbol: '$', + }); + + const eurCommodity = await this.createCommodity(user.id, { + code: CommodityCode.create('EUR').valueOf(), + name: 'Euro', + precision: 2, + symbol: '€', + }); + + const accountUSD1 = await this.createAccount(user.id, usdCommodity.id, { currency: Currency.create('USD').valueOf(), name: 'Savings Account USD', }); - const accountUSD2 = await this.createAccount(user.id, { + const accountUSD2 = await this.createAccount(user.id, usdCommodity.id, { currency: Currency.create('USD').valueOf(), name: 'Checking Account USD', }); - const accountEUR = await this.createAccount(user.id, { + const accountEUR = await this.createAccount(user.id, eurCommodity.id, { currency: Currency.create('EUR').valueOf(), name: 'Credit Card EUR', }); diff --git a/apps/backend/src/db/test-utils/testEntityBuilder.ts b/apps/backend/src/db/test-utils/testEntityBuilder.ts index 55bf919a..8f9f9378 100644 --- a/apps/backend/src/db/test-utils/testEntityBuilder.ts +++ b/apps/backend/src/db/test-utils/testEntityBuilder.ts @@ -1,11 +1,29 @@ -import { UUID, CurrencyCode, IsoDateString } from '@ledgerly/shared/types'; +import { + UUID, + CurrencyCode, + IsoDateString, + CommodityCodeString, +} from '@ledgerly/shared/types'; import { CreateOperationRequestDTO, CreateTransactionRequestDTO, EntityNotFoundError, } from 'src/application'; -import { Account, AccountType, Operation, Transaction, User } from 'src/domain'; -import { Amount, Currency, DateValue, Name } from 'src/domain/domain-core'; +import { + Account, + AccountType, + Commodity, + Operation, + Transaction, + User, +} from 'src/domain'; +import { + Amount, + CommodityCode, + Currency, + DateValue, + Name, +} from 'src/domain/domain-core'; import { CreateTransactionProps, TransactionBuildContext, @@ -37,8 +55,10 @@ export type TransactionBuilderOptions = { export type TransactionRequestBuilderResult = { accountsMap: Map; accounts: Account[]; + commodities: Commodity[]; transactionContext: TransactionBuildContext; getAccountByKey: (key: string) => Account; + getCommodityByKey: (code: string) => Commodity; getSystemAccountByCurrency: (currency: string) => Account; systemAccounts: Map; user: User; @@ -65,6 +85,7 @@ export class TransactionBuilder { private readonly user: User; private readonly accounts = new Map(); private readonly accountsMap = new Map(); + private readonly commoditiesMap = new Map(); private readonly systemAccounts = new Map(); private readonly operationsData: OperationDataForTransaction[]; private readonly postingDate: IsoDateString; @@ -109,8 +130,16 @@ export class TransactionBuilder { private createAccounts(currencyCodes: string[]): void { currencyCodes.forEach((currencyCode) => { + const code = CommodityCode.create(currencyCode); + const commodity = Commodity.create( + this.user, + Name.create(`Commodity ${currencyCode}`), + code, + ); + const account = Account.create( this.user, + commodity, Name.create(`Account ${currencyCode}`), `Account ${currencyCode}`, Amount.create('0'), @@ -120,16 +149,30 @@ export class TransactionBuilder { this.accounts.set(currencyCode, account); this.accountsMap.set(account.getId().valueOf(), account); + this.commoditiesMap.set(code.valueOf(), commodity); }); this.createSystemAccounts(); } + private getCommodityByCurrency(currency: string): Commodity { + const code = CommodityCode.create(currency); + const commodity = this.commoditiesMap.get(code.valueOf()); + + if (!commodity) { + throw new EntityNotFoundError({ entityType: Commodity.entityType }); + } + + return commodity; + } + private createSystemAccounts(): void { this.accounts.forEach((account) => { const currencyCode = account.currency.valueOf(); + const commodity = this.getCommodityByCurrency(currencyCode); const systemAccount = Account.create( this.user, + commodity, Name.create(`System Account ${currencyCode}`), `System Account for ${currencyCode}`, Amount.create('0'), @@ -162,6 +205,16 @@ export class TransactionBuilder { return account; } + getCommodityByKey(code: string): Commodity { + const commodity = this.commoditiesMap.get(code as CommodityCodeString); + + if (!commodity) { + throw new EntityNotFoundError({ entityType: Commodity.entityType }); + } + + return commodity; + } + private buildOperationsDTO(): CreateOperationRequestDTO[] { return this.operationsData.map((operation) => { const amount = Amount.create(operation.amount).valueOf(); @@ -201,7 +254,9 @@ export class TransactionBuilder { return { accounts: Array.from(this.accountsMap.values()), accountsMap: this.accountsMap, + commodities: Array.from(this.commoditiesMap.values()), getAccountByKey: this.getAccountByKey.bind(this), + getCommodityByKey: this.getCommodityByKey.bind(this), getSystemAccountByCurrency: this.getSystemAccountByCurrency.bind(this), operationsData: this.operationsData, systemAccounts: this.systemAccounts, diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index b8741a9b..fddd1558 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -1,5 +1,4 @@ import { LoginUserUseCase, RegisterUserUseCase } from 'src/application'; -import { AccountFactory } from 'src/application/services'; import { TransactionContextLoader } from 'src/application/services/TransactionService'; import { ensureEntityExistsAndOwned } from 'src/application/shared/ensureEntityExistsAndOwned'; import { CreateAccountUseCase } from 'src/application/usecases/accounts/createAccount'; @@ -23,6 +22,7 @@ import { AccountRepository, UserRepository, } from 'src/infrastructure/db'; +import { CommodityRepository } from 'src/infrastructure/db/commodities/commodity.repository'; import { AccountController, AuthController, @@ -38,6 +38,7 @@ export const createContainer = (db: DataBase): AppContainer => { const accountRepository = new AccountRepository(transactionManager); const currencyRepository = new CurrencyRepository(transactionManager); + const commodityRepository = new CommodityRepository(transactionManager); const operationRepository = new OperationRepository(transactionManager); const transactionRepository = new TransactionRepository( operationRepository, @@ -52,6 +53,7 @@ export const createContainer = (db: DataBase): AppContainer => { const repositories: AppContainer['repositories'] = { account: accountRepository, + commodity: commodityRepository, currency: currencyRepository, transaction: transactionRepository, transactionQuery: transactionQueryRepository, @@ -60,8 +62,6 @@ export const createContainer = (db: DataBase): AppContainer => { // Services and Factories - const accountFactory = new AccountFactory(accountRepository); - const transactionContextLoader = new TransactionContextLoader( accountRepository, ); @@ -73,7 +73,10 @@ export const createContainer = (db: DataBase): AppContainer => { }; // Create Account Use Cases - const createAccountUseCase = new CreateAccountUseCase(accountFactory); + const createAccountUseCase = new CreateAccountUseCase( + accountRepository, + commodityRepository, + ); const getAllAccountsUseCase = new GetAllAccountsUseCase(accountRepository); const getAccountByIdUseCase = new GetAccountByIdUseCase(accountRepository); const updateAccountUseCase = new UpdateAccountUseCase(accountRepository); @@ -161,14 +164,9 @@ export const createContainer = (db: DataBase): AppContainer => { user: userController, }; - const factories: AppContainer['factories'] = { - account: accountFactory, - }; - return { controllers, db, - factories, repositories, services, useCases, diff --git a/apps/backend/src/di/types.ts b/apps/backend/src/di/types.ts index d54febc5..482e1adb 100644 --- a/apps/backend/src/di/types.ts +++ b/apps/backend/src/di/types.ts @@ -1,5 +1,4 @@ import { LoginUserUseCase, RegisterUserUseCase } from 'src/application'; -import { AccountFactory } from 'src/application/services'; import { CreateAccountUseCase } from 'src/application/usecases/accounts/createAccount'; import { DeleteAccountUseCase } from 'src/application/usecases/accounts/deleteAccount'; import { GetAccountByIdUseCase } from 'src/application/usecases/accounts/getAccountById'; @@ -19,6 +18,7 @@ import { AccountRepository, CurrencyRepository, TransactionRepository, + CommodityRepository, UserRepository, } from 'src/infrastructure/db'; import { @@ -31,6 +31,7 @@ import { type Repositories = { currency: CurrencyRepository; transaction: TransactionRepository; + commodity: CommodityRepository; transactionQuery: TransactionQueryRepository; account: AccountRepository; user: UserRepository; @@ -68,10 +69,6 @@ type Controllers = { transaction: TransactionController; }; -type Factories = { - account: AccountFactory; -}; - export type AppContainer = { db: DataBase; repositories: Repositories; @@ -82,5 +79,4 @@ export type AppContainer = { transaction: TransactionUseCases; }; controllers: Controllers; - factories: Factories; }; diff --git a/apps/backend/src/domain/accounts/account.entity.ts b/apps/backend/src/domain/accounts/account.entity.ts index ed57f471..67a7823e 100644 --- a/apps/backend/src/domain/accounts/account.entity.ts +++ b/apps/backend/src/domain/accounts/account.entity.ts @@ -1,3 +1,4 @@ +import { Commodity } from '../commodities'; import { Amount, Currency, @@ -18,33 +19,24 @@ import { AccountSnapshot, AccountUpdateProps } from './types'; export class Account { static readonly entityType = 'account'; - private readonly identity: EntityIdentity; - private timestamps: EntityTimestamps; - private softDelete: SoftDelete; - private readonly ownership: ParentChildRelation; - private constructor( - identity: EntityIdentity, - timestamps: EntityTimestamps, - softDelete: SoftDelete, - ownership: ParentChildRelation, + private readonly identity: EntityIdentity, + private timestamps: EntityTimestamps, + private softDelete: SoftDelete, + private readonly ownership: ParentChildRelation, + private readonly commodityRelation: ParentChildRelation, public name: Name, public description: string, private initialBalance: Amount, - // remove currentClearedBalanceLocal from entity and schemas later private currentClearedBalanceLocal: Amount, public currency: Currency, private type: AccountType, public isSystem: boolean, - ) { - this.identity = identity; - this.timestamps = timestamps; - this.softDelete = softDelete; - this.ownership = ownership; - } + ) {} static create( user: User, + commodity: Commodity, name: Name, description: string, initialBalance: Amount, @@ -60,6 +52,11 @@ export class Account { identity.getId(), ); + const commodityRelation = ParentChildRelation.create( + commodity.getId(), + identity.getId(), + ); + const isSystem = type.isSystemType(); return new Account( @@ -67,6 +64,7 @@ export class Account { timestamps, softDelete, ownership, + commodityRelation, name, description, initialBalance, @@ -79,6 +77,7 @@ export class Account { static restore(data: AccountSnapshot): Account { const { + commodityId, createdAt, currency, currentClearedBalanceLocal, @@ -107,11 +106,17 @@ export class Account { identity.getId(), ); + const commodityRelation = ParentChildRelation.create( + Id.restore(commodityId), + identity.getId(), + ); + return new Account( identity, timestamps, softDelete, ownership, + commodityRelation, Name.restore(name), description, Amount.restore(initialBalance), @@ -162,12 +167,9 @@ export class Account { return this.ownership.belongsToParent(userId); } - getUserId(): Id { - return this.ownership.getParentId(); - } - toSnapshot(): AccountSnapshot { return { + commodityId: this.commodityRelation.getParentId().valueOf(), createdAt: this.getCreatedAt().valueOf(), currency: this.currency.valueOf(), currentClearedBalanceLocal: this.currentClearedBalanceLocal.valueOf(), @@ -208,7 +210,7 @@ export class Account { return this.currency.valueOf() === currency.valueOf(); } - getCurrency(): Currency { - return this.currency; + isCommoditySame(commodity: Commodity): boolean { + return this.commodityRelation.getParentId().equals(commodity.getId()); } } diff --git a/apps/backend/src/domain/accounts/account.test.ts b/apps/backend/src/domain/accounts/account.test.ts index c929250a..728dda4a 100644 --- a/apps/backend/src/domain/accounts/account.test.ts +++ b/apps/backend/src/domain/accounts/account.test.ts @@ -1,15 +1,19 @@ import { createUser } from 'src/db/createTestUser'; -import { AccountType } from 'src/domain/'; -import { Amount, Name, Currency, Id, Timestamp } from 'src/domain/domain-core/'; +import { AccountType, Commodity } from 'src/domain/'; +import { + Amount, + Name, + Currency, + Id, + Timestamp, + CommodityCode, +} from 'src/domain/domain-core/'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { User } from '../users/user.entity'; import { Account } from './account.entity'; -const userIdValue = Id.restore( - '123e4567-e89b-12d3-a456-426614174000', -).valueOf(); const userTypeValue = 'asset'; const currencyUSD = Currency.create('USD'); @@ -23,10 +27,18 @@ describe('Account Domain Entity', () => { const accountType = AccountType.create(userTypeValue); let user: User; + let commodity: Commodity; let userId: ReturnType; beforeAll(async () => { user = await createUser(); + commodity = Commodity.create( + user, + Name.create('commodity-name'), + CommodityCode.create('COM'), + 2, + 'C', + ); userId = user.getId(); }); @@ -38,6 +50,7 @@ describe('Account Domain Entity', () => { it('should create account with valid data', () => { const account = Account.create( user, + commodity, name, 'account-description', Amount.create('0'), @@ -47,15 +60,15 @@ describe('Account Domain Entity', () => { expect(account).toBeInstanceOf(Account); expect(account.getId()).toBeDefined(); - expect(account.getUserId()).toBe(userId); + expect(account.belongsToUser(userId)).toBe(true); expect(account).toHaveProperty('name', name); expect(account).toHaveProperty('description', 'account-description'); expect(account).toHaveProperty('initialBalance', Amount.create('0')); expect(account).toHaveProperty('currency', currencyUSD); - expect(account.getType().valueOf()).toBe(userTypeValue); - expect(account.getUserId().equals(userId)).toBe(true); + expect(account.getType().valueOf()).toBe(accountType.valueOf()); expect(account.belongsToUser(userId)).toBe(true); expect(account.getType().equals(accountType)).toBe(true); + expect(account.isCommoditySame(commodity)).toBe(true); }); }); @@ -70,6 +83,7 @@ describe('Account Domain Entity', () => { const updatedAt = Timestamp.restore(updatedAtValue); const account = Account.restore({ + commodityId: commodity.getId().valueOf(), createdAt: createdAt.valueOf(), currency: currencyCodeEUR, currentClearedBalanceLocal: Amount.create('500').valueOf(), @@ -81,12 +95,12 @@ describe('Account Domain Entity', () => { name: 'restored-account', type: userTypeValue, updatedAt: updatedAt.valueOf(), - userId: userIdValue, + userId: userId.valueOf(), }); expect(account).toBeInstanceOf(Account); expect(account.getId().toString()).toBe(accountIdValue); - expect(account.getUserId().toString()).toBe(userIdValue); + expect(account.belongsToUser(userId)).toBe(true); expect(account).toHaveProperty('name', Name.create('restored-account')); expect(account).toHaveProperty('description', 'restored-description'); expect(account).toHaveProperty('initialBalance', Amount.create('500')); @@ -98,6 +112,7 @@ describe('Account Domain Entity', () => { it('should update account with valid value', () => { const account = Account.create( user, + commodity, Name.create('initial-name'), 'description', Amount.create('0'), @@ -115,6 +130,7 @@ describe('Account Domain Entity', () => { it('should mark account as tombstone', () => { const account = Account.create( user, + commodity, name, 'account-description', Amount.create('0'), @@ -132,6 +148,7 @@ describe('Account Domain Entity', () => { it('should not allow updates after soft deletion', () => { const account = Account.create( user, + commodity, name, 'account-description', Amount.create('0'), @@ -149,6 +166,7 @@ describe('Account Domain Entity', () => { it('should not allow deleting an already deleted account', () => { const account = Account.create( user, + commodity, name, 'account-description', Amount.create('0'), @@ -185,6 +203,7 @@ describe('Account Domain Entity', () => { const account = Account.create( user, + commodity, name, 'account-description', Amount.create('0'), diff --git a/apps/backend/src/domain/accounts/types.ts b/apps/backend/src/domain/accounts/types.ts index 211b9a82..f0ff8730 100644 --- a/apps/backend/src/domain/accounts/types.ts +++ b/apps/backend/src/domain/accounts/types.ts @@ -10,6 +10,7 @@ export type AccountSnapshot = { createdAt: IsoDatetimeString; currency: CurrencyCode; currentClearedBalanceLocal: AmountString; + commodityId: UUID; description: string; id: UUID; initialBalance: AmountString; diff --git a/apps/backend/src/domain/commodities/index.ts b/apps/backend/src/domain/commodities/index.ts index adfe6c1b..eada130c 100644 --- a/apps/backend/src/domain/commodities/index.ts +++ b/apps/backend/src/domain/commodities/index.ts @@ -1 +1,2 @@ export { Commodity } from './commodity.entity'; +export { CommoditySnapshot } from './types'; diff --git a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts index 08939b8d..8273e429 100644 --- a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts @@ -6,6 +6,7 @@ import { AccountPersistenceMapper } from './account-persistence.mapper'; describe('AccountPersistenceMapper', () => { const row: AccountDbRow = { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency: Currency.create('USD').valueOf(), currentClearedBalanceLocal: Amount.create('2500').valueOf(), @@ -22,6 +23,7 @@ describe('AccountPersistenceMapper', () => { it('maps a persistence row to an account snapshot explicitly', () => { expect(AccountPersistenceMapper.toSnapshot(row)).toEqual({ + commodityId: row.commodityId, createdAt: row.createdAt, currency: row.currency, currentClearedBalanceLocal: row.currentClearedBalanceLocal, @@ -51,6 +53,7 @@ describe('AccountPersistenceMapper', () => { expect( AccountPersistenceMapper.toDBRowFromSnapshot(account.toSnapshot()), ).toEqual({ + commodityId: row.commodityId, createdAt: row.createdAt, currency: row.currency, currentClearedBalanceLocal: row.currentClearedBalanceLocal, diff --git a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts index f3db5a78..ca6dd5a0 100644 --- a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts +++ b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts @@ -8,6 +8,7 @@ export class AccountPersistenceMapper { static toSnapshot(row: AccountDbRow): AccountSnapshot { return { + commodityId: row.commodityId, createdAt: row.createdAt, currency: row.currency, currentClearedBalanceLocal: row.currentClearedBalanceLocal, @@ -25,6 +26,7 @@ export class AccountPersistenceMapper { static toDBRowFromSnapshot(snapshot: AccountSnapshot): AccountRepoInsert { return { + commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, currency: snapshot.currency, currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal, diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts index 2ceaadf1..e24de730 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts @@ -5,13 +5,17 @@ import { UUID, } from '@ledgerly/shared/types'; import dayjs from 'dayjs'; -import { AccountDbInsert, AccountDbRow, UserDbRow } from 'src/db/schema'; -import { Amount, Timestamp } from 'src/domain/domain-core'; +import { + AccountDbInsert, + AccountDbRow, + CommodityDbRow, + UserDbRow, +} from 'src/db/schema'; +import { Amount, CommodityCode, Timestamp } from 'src/domain/domain-core'; import { Currency } from 'src/domain/domain-core/value-objects/Currency'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { AccountRepository } from 'src/infrastructure/db/'; import { - ForeignKeyConstraintError, RecordAlreadyExistsError, RepositoryNotFoundError, } from 'src/infrastructure/errors'; @@ -27,11 +31,13 @@ const USD: CurrencyCode = 'USD' as CurrencyCode; const getAccountData = (params: { userId: UUID; + commodityId: UUID; name: string; currency: CurrencyCode; type: AccountTypeValue; }): AccountDbInsert => { return { + commodityId: params.commodityId, currency: params.currency, currentClearedBalanceLocal: Amount.create('0').valueOf(), description: 'This is a test account', @@ -47,6 +53,7 @@ const getAccountData = (params: { }; const accountDataRaw = { + commodityId: 'non-existent-commodity-id' as UUID, currency: USD, name: 'Test Account', type: 'asset' as const, @@ -70,17 +77,28 @@ describe('AccountRepository', () => { ); let user: UserDbRow; + let usdCommodity: CommodityDbRow; + let eurCommodity: CommodityDbRow; beforeEach(async () => { testDB = new TestDB(); await testDB.setupTestDb(); user = await testDB.createUser(); + + usdCommodity = await testDB.createCommodity(user.id, { + code: CommodityCode.create('USD').valueOf(), + }); + + eurCommodity = await testDB.createCommodity(user.id, { + code: CommodityCode.create('EUR').valueOf(), + }); }); describe('create', () => { it('should create a new account successfully', async () => { const newAccount = getAccountData({ + commodityId: usdCommodity.id, currency: USD, name: 'New Account', type: 'asset', @@ -98,15 +116,35 @@ describe('AccountRepository', () => { expect(account.userId).toBe(newAccount.userId); }); + it("should throw an error if commodity belongs to a different user than the account's user", async () => { + const secondUser = await testDB.createUser({ + email: 'second-user@example.com', + name: 'Second User', + }); + + const newAccount = getAccountData({ + commodityId: usdCommodity.id, + currency: USD, + name: 'New Account', + type: 'asset', + userId: secondUser.id, + }); + + await expect(accountRepository.create(newAccount)).rejects.toThrowError( + RepositoryNotFoundError, + ); + }); + it('should not allow duplicate account names for the same user', async () => { const newAccount = getAccountData({ + commodityId: usdCommodity.id, currency: USD, name: 'New Account', type: 'asset', userId: user.id, }); - await testDB.createAccount(user.id, newAccount); + await testDB.createAccount(user.id, usdCommodity.id, newAccount); await expect(accountRepository.create(newAccount)).rejects.toThrowError( new RecordAlreadyExistsError({ @@ -128,6 +166,7 @@ describe('AccountRepository', () => { }); const firstUserAccount = getAccountData({ + commodityId: usdCommodity.id, currency: USD, name: accountName, type: 'asset', @@ -135,18 +174,23 @@ describe('AccountRepository', () => { }); const secondUserAccount = getAccountData({ + commodityId: usdCommodity.id, currency: USD, name: accountName, type: 'asset', userId: secondUser.id, }); - const account1 = await testDB.createAccount(user.id, { + const account1 = await testDB.createAccount(user.id, usdCommodity.id, { ...firstUserAccount, }); - const account2 = await testDB.createAccount(secondUser.id, { - ...secondUserAccount, - }); + const account2 = await testDB.createAccount( + secondUser.id, + usdCommodity.id, + { + ...secondUserAccount, + }, + ); expect(account1).toHaveProperty('id'); expect(account1.name).toBe(accountName); @@ -162,25 +206,6 @@ describe('AccountRepository', () => { it.todo('should throw an error if the original currency does not exist'); - it('should throw an error if the user does not exist', async () => { - const newAccount = getAccountData({ - currency: USD, - name: 'New Account', - type: 'asset', - userId: Id.create().valueOf(), - }); - - await expect(accountRepository.create(newAccount)).rejects.toThrowError( - new ForeignKeyConstraintError({ - context: { - field: 'userId', - tableName: 'accounts', - value: newAccount.userId, - }, - }), - ); - }); - it.todo('should handle UUID collision gracefully'); }); @@ -192,7 +217,7 @@ describe('AccountRepository', () => { }); for (const name of firstUserAccounts) { - await testDB.createAccount(user.id, { + await testDB.createAccount(user.id, usdCommodity.id, { currency: USD, name, type: 'asset', @@ -200,7 +225,7 @@ describe('AccountRepository', () => { } for (const name of secondUserAccounts) { - await testDB.createAccount(secondUser.id, { + await testDB.createAccount(secondUser.id, usdCommodity.id, { currency: USD, name, type: 'asset', @@ -241,7 +266,7 @@ describe('AccountRepository', () => { let account: AccountDbRow; beforeEach(async () => { - account = await testDB.createAccount(user.id); + account = await testDB.createAccount(user.id, usdCommodity.id, {}); }); it('should retrieve an account by ID', async () => { @@ -297,13 +322,14 @@ describe('AccountRepository', () => { let account: AccountDbRow; beforeEach(async () => { - account = await testDB.createAccount(user.id); + account = await testDB.createAccount(user.id, usdCommodity.id, {}); }); it('should update account when it belongs to user', async () => { const user2 = await testDB.createUser(); const updatedAccountData = getAccountData({ + commodityId: eurCommodity.id, currency: 'EUR' as CurrencyCode, name: 'Updated Account', type: 'expense', @@ -326,6 +352,7 @@ describe('AccountRepository', () => { it('should return undefined when account belongs to different user', async () => { const updatedAccountData = getAccountData({ + commodityId: eurCommodity.id, currency: 'EUR' as CurrencyCode, name: 'Updated Account', type: 'expense', @@ -345,13 +372,14 @@ describe('AccountRepository', () => { it('should not allow updating to duplicate name within same user', async () => { const updatedAccountData = getAccountData({ + commodityId: usdCommodity.id, currency: USD, name: 'Updated Account', type: 'asset', userId: user.id, }); - await testDB.createAccount(user.id, { + await testDB.createAccount(user.id, usdCommodity.id, { currency: USD, initialBalance: Amount.create('200').valueOf(), name: updatedAccountData.name, @@ -377,12 +405,16 @@ describe('AccountRepository', () => { name: 'Second User', }); - const secondUserAccount = await testDB.createAccount(secondUser.id, { - currency: USD, - initialBalance: Amount.create('200').valueOf(), - name: 'Shared Account Name', - type: 'asset', - }); + const secondUserAccount = await testDB.createAccount( + secondUser.id, + usdCommodity.id, + { + currency: USD, + initialBalance: Amount.create('200').valueOf(), + name: 'Shared Account Name', + type: 'asset', + }, + ); const updatedSecondUserAccount = await accountRepository.update( secondUser.id, @@ -430,7 +462,7 @@ describe('AccountRepository', () => { let account: AccountDbRow; beforeEach(async () => { - account = await testDB.createAccount(user.id); + account = await testDB.createAccount(user.id, usdCommodity.id); }); it('should delete account when it exists and belongs to user', async () => { @@ -467,7 +499,7 @@ describe('AccountRepository', () => { name: 'Second User', }); - await testDB.createAccount(secondUser.id, { + await testDB.createAccount(secondUser.id, usdCommodity.id, { currency: USD, initialBalance: Amount.create('2000').valueOf(), name: 'Shared Account Name', @@ -499,6 +531,7 @@ describe('AccountRepository', () => { const beforeCreate = dayjs(); const newAccount = getAccountData({ + commodityId: usdCommodity.id, currency: 'USD' as CurrencyCode, name: 'This is a test account', type: 'asset', @@ -519,12 +552,13 @@ describe('AccountRepository', () => { }); it('should update updatedAt on account update', async () => { - const account = await testDB.createAccount(user.id); + const account = await testDB.createAccount(user.id, usdCommodity.id); const originalUpdatedAt = dayjs(account?.updatedAt); await new Promise((resolve) => setTimeout(resolve, 1000)); const updatedData = getAccountData({ + commodityId: eurCommodity.id, currency: 'EUR' as CurrencyCode, name: 'Updated Account', type: 'expense', @@ -548,16 +582,21 @@ describe('AccountRepository', () => { describe('findSystemAccount', () => { it('should retrieve system account by currency for a user', async () => { const systemAccountData = getAccountData({ + commodityId: usdCommodity.id, currency: USD, name: 'System Account for USD', type: 'asset', userId: user.id, }); - const systemAccount = await testDB.createAccount(user.id, { - ...systemAccountData, - isSystem: true, - }); + const systemAccount = await testDB.createAccount( + user.id, + usdCommodity.id, + { + ...systemAccountData, + isSystem: true, + }, + ); const retrievedAccount = await accountRepository.findSystemAccount( user.id, diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.ts index 1f171dab..5e587c48 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.ts @@ -6,6 +6,7 @@ import { type AccountRepositoryUpdateInput, } from 'src/application'; import { accountsTable } from 'src/db/schemas/accounts'; +import { commoditiesTable } from 'src/db/schemas/commodities'; import { AccountSnapshot } from 'src/domain/accounts'; import { BaseRepository } from '../BaseRepository'; @@ -38,6 +39,24 @@ export class AccountRepository create(data: AccountSnapshot): Promise { return this.executeDatabaseOperation( async () => { + const existingCommodity = await this.db + .select() + .from(commoditiesTable) + .where( + and( + eq(commoditiesTable.id, data.commodityId), + eq(commoditiesTable.userId, data.userId), + eq(commoditiesTable.isTombstone, false), + ), + ) + .get(); + + this.ensureEntityExists( + existingCommodity, + `Commodity with ID ${data.commodityId} not found`, + this.entityNotFoundContext('commodity', data.commodityId), + ); + const account = await this.db .insert(accountsTable) .values({ diff --git a/apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.test.ts new file mode 100644 index 00000000..7c18c909 --- /dev/null +++ b/apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.test.ts @@ -0,0 +1,61 @@ +import { CommodityDbRow } from 'src/db/schema'; +import { CommodityCode } from 'src/domain/domain-core'; +import { Id } from 'src/domain/domain-core/value-objects/Id'; +import { Timestamp } from 'src/domain/domain-core/value-objects/Timestamp'; +import { describe, expect, it } from 'vitest'; + +import { CommodityPersistenceMapper } from './commodity-persistence.mapper'; + +describe('CommodityPersistenceMapper', () => { + const row: CommodityDbRow = { + code: CommodityCode.create('USD').valueOf(), + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: 'US Dollar', + precision: 2, + symbol: '$', + updatedAt: Timestamp.create().valueOf(), + userId: Id.create().valueOf(), + }; + + it('maps a persistence row to a commodity snapshot explicitly', () => { + expect(CommodityPersistenceMapper.toSnapshot(row)).toEqual({ + code: row.code, + createdAt: row.createdAt, + id: row.id, + isTombstone: row.isTombstone, + name: row.name, + precision: row.precision, + symbol: row.symbol, + updatedAt: row.updatedAt, + userId: row.userId, + }); + }); + + it('maps a persistence row to a domain commodity snapshot', () => { + const commodity = CommodityPersistenceMapper.toDomain(row); + + expect(commodity.toSnapshot()).toEqual( + CommodityPersistenceMapper.toSnapshot(row), + ); + }); + + it('maps a domain commodity snapshot to a persistence row', () => { + const commodity = CommodityPersistenceMapper.toDomain(row); + + expect( + CommodityPersistenceMapper.toDBRowFromSnapshot(commodity.toSnapshot()), + ).toEqual({ + code: row.code, + createdAt: row.createdAt, + id: row.id, + isTombstone: row.isTombstone, + name: row.name, + precision: row.precision, + symbol: row.symbol, + updatedAt: row.updatedAt, + userId: row.userId, + }); + }); +}); diff --git a/apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.ts b/apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.ts new file mode 100644 index 00000000..21914b91 --- /dev/null +++ b/apps/backend/src/infrastructure/db/commodities/commodity-persistence.mapper.ts @@ -0,0 +1,37 @@ +import { CommodityDbInsert, CommodityDbRow } from 'src/db/schema'; +import { Commodity } from 'src/domain'; +import { CommoditySnapshot } from 'src/domain/commodities/types'; + +export class CommodityPersistenceMapper { + static toDomain(row: CommodityDbRow): Commodity { + return Commodity.restore(CommodityPersistenceMapper.toSnapshot(row)); + } + + static toSnapshot(row: CommodityDbRow): CommoditySnapshot { + return { + code: row.code, + createdAt: row.createdAt, + id: row.id, + isTombstone: row.isTombstone, + name: row.name, + precision: row.precision, + symbol: row.symbol, + updatedAt: row.updatedAt, + userId: row.userId, + }; + } + + static toDBRowFromSnapshot(snapshot: CommoditySnapshot): CommodityDbInsert { + return { + code: snapshot.code, + createdAt: snapshot.createdAt, + id: snapshot.id, + isTombstone: snapshot.isTombstone, + name: snapshot.name, + precision: snapshot.precision, + symbol: snapshot.symbol, + updatedAt: snapshot.updatedAt, + userId: snapshot.userId, + }; + } +} diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts new file mode 100644 index 00000000..9ece0f8b --- /dev/null +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -0,0 +1,79 @@ +import { apiErrorCodes } from '@ledgerly/shared/types'; +import type { CommodityDbRow } from 'src/db/schemas/commodities'; +import { UserDbRow } from 'src/db/schemas/users'; +import { Id } from 'src/domain/domain-core/value-objects/Id'; +import { RepositoryNotFoundError } from 'src/infrastructure/errors'; +import { describe, vi, beforeEach, it, expect } from 'vitest'; + +import { TestDB } from '../../../db/test-db'; +import { TransactionManager } from '../TransactionManager'; + +import { CommodityRepository } from './commodity.repository'; + +describe('CommodityRepository', () => { + let testDB: TestDB; + + const transactionManager = { + getCurrentTransaction: () => testDB.db, + run: vi.fn((cb: () => unknown) => { + return cb(); + }), + }; + + const commodityRepository = new CommodityRepository( + transactionManager as unknown as TransactionManager, + ); + + let user: UserDbRow; + + beforeEach(async () => { + testDB = new TestDB(); + await testDB.setupTestDb(); + + user = await testDB.createUser(); + }); + + describe('getById', () => { + let commodityDbRow: CommodityDbRow; + + beforeEach(async () => { + commodityDbRow = await testDB.createCommodity(user.id); + }); + + it('should return the commodity when it exists', async () => { + const retrievedCommodity = await commodityRepository.getById( + user.id, + commodityDbRow.id, + ); + + expect(retrievedCommodity).toEqual(commodityDbRow); + }); + + it('should throw an error when the commodity does not exist', async () => { + const nonExistentId = Id.create().valueOf(); + + await expect( + commodityRepository.getById(user.id, nonExistentId), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('returns an allowlisted error contract for a missing commodity', async () => { + const nonExistentId = Id.create().valueOf(); + + await expect( + commodityRepository.getById(user.id, nonExistentId), + ).rejects.toMatchObject({ + code: apiErrorCodes.entityNotFound, + context: { entityId: nonExistentId, entityType: 'commodity' }, + }); + }); + + it('should throw an error when the commodity belongs to a different user', async () => { + const anotherUser = await testDB.createUser(); + + await expect( + commodityRepository.getById(anotherUser.id, commodityDbRow.id), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + }); +}); diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts new file mode 100644 index 00000000..2b195e53 --- /dev/null +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -0,0 +1,34 @@ +import { UUID } from '@ledgerly/shared/types'; +import { and, eq } from 'drizzle-orm'; +import { type CommodityRepositoryInterface } from 'src/application'; +import { commoditiesTable } from 'src/db/schemas/commodities'; +import { CommoditySnapshot } from 'src/domain/commodities/types'; + +import { BaseRepository } from '../BaseRepository'; + +import { CommodityPersistenceMapper } from './commodity-persistence.mapper'; + +export class CommodityRepository + extends BaseRepository + implements CommodityRepositoryInterface +{ + getById(userId: UUID, id: UUID): Promise { + return this.executeDatabaseOperation(async () => { + const commodity = await this.db + .select() + .from(commoditiesTable) + .where( + and(eq(commoditiesTable.userId, userId), eq(commoditiesTable.id, id)), + ) + .get(); + + const existingCommodity = this.ensureEntityExists( + commodity, + `Commodity with ID ${id} not found`, + this.entityNotFoundContext('commodity', id), + ); + + return CommodityPersistenceMapper.toSnapshot(existingCommodity); + }, 'Failed to fetch commodity'); + } +} diff --git a/apps/backend/src/infrastructure/db/commodities/index.ts b/apps/backend/src/infrastructure/db/commodities/index.ts new file mode 100644 index 00000000..872136e3 --- /dev/null +++ b/apps/backend/src/infrastructure/db/commodities/index.ts @@ -0,0 +1,2 @@ +export { CommodityPersistenceMapper } from './commodity-persistence.mapper'; +export { CommodityRepository } from './commodity.repository'; diff --git a/apps/backend/src/infrastructure/db/index.ts b/apps/backend/src/infrastructure/db/index.ts index a4433289..daf935ba 100644 --- a/apps/backend/src/infrastructure/db/index.ts +++ b/apps/backend/src/infrastructure/db/index.ts @@ -8,3 +8,4 @@ export { OperationRepository } from './operations'; export { AccountRepository } from './accounts'; export { CurrencyRepository } from './currency'; export { UserRepository } from './user'; +export { CommodityRepository } from './commodities'; diff --git a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts index 4e41e90c..83180613 100644 --- a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts +++ b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts @@ -13,6 +13,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TestDB } from '../../../db/test-db'; import { AccountPersistenceMapper } from '../accounts'; +import { CommodityPersistenceMapper } from '../commodities'; import { TransactionManager } from '../TransactionManager'; import { UserPersistenceMapper } from '../user'; @@ -60,6 +61,16 @@ describe('OperationRepository', () => { transaction = data.transaction; + await Promise.all( + data.commodities.map((commodity) => + testDB.insertCommodity( + CommodityPersistenceMapper.toDBRowFromSnapshot( + commodity.toSnapshot(), + ), + ), + ), + ); + await Promise.all( data.accounts.map((account) => testDB.insertAccount( diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts index 6c9558d6..2cb619b8 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts @@ -2,6 +2,7 @@ import { DEFAULT_TRANSACTION_QUERY } from '@ledgerly/shared/constants'; import { CurrencyCode, IsoDateString, UUID } from '@ledgerly/shared/types'; import { UserDbRow } from 'src/db/schema'; import { TestDB, TransactionSeed } from 'src/db/test-db'; +import { CommodityCode, Name } from 'src/domain/domain-core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TransactionManager, TransactionQueryRepository } from '../'; @@ -26,11 +27,10 @@ describe('TransactionQueryRepository', () => { }; const createAccount = async ( - currency: CurrencyCode, name: string, + commodityId: UUID, ): Promise => { - const account = await testDB.createAccount(user.id, { - currency, + const account = await testDB.createAccount(user.id, commodityId, { name, }); @@ -80,8 +80,26 @@ describe('TransactionQueryRepository', () => { await testDB.setupTestDb(); user = await testDB.createUser(); - usdAccount = await createAccount('USD' as CurrencyCode, 'Cash USD'); - eurAccount = await createAccount('EUR' as CurrencyCode, 'Cash EUR'); + + const usdCommodityId = ( + await testDB.createCommodity(user.id, { + code: CommodityCode.create('USD').valueOf(), + }) + ).id; + const eurCommodityId = ( + await testDB.createCommodity(user.id, { + code: CommodityCode.create('EUR').valueOf(), + }) + ).id; + + usdAccount = await createAccount( + Name.create('USD').valueOf(), + usdCommodityId, + ); + eurAccount = await createAccount( + Name.create('EUR').valueOf(), + eurCommodityId, + ); transactionQueryRepo = new TransactionQueryRepository( transactionManager as unknown as TransactionManager, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts index 999771ba..d51a7b6f 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts @@ -21,6 +21,7 @@ import { TransactionRepository, } from '../'; import { AccountPersistenceMapper } from '../accounts'; +import { CommodityPersistenceMapper } from '../commodities'; import { OperationPersistenceMapper } from '../operations'; import { UserPersistenceMapper } from '../user'; @@ -85,6 +86,16 @@ describe('TransactionRepository', () => { user: UserPersistenceMapper.toDomain(user), }); + const usdCommodity = data.getCommodityByKey('USD'); + const eurCommodity = data.getCommodityByKey('EUR'); + + await testDB.insertCommodity( + CommodityPersistenceMapper.toDBRowFromSnapshot(usdCommodity.toSnapshot()), + ); + await testDB.insertCommodity( + CommodityPersistenceMapper.toDBRowFromSnapshot(eurCommodity.toSnapshot()), + ); + usdAccount = data.getAccountByKey('USD'); eurAccount = data.getAccountByKey('EUR'); diff --git a/apps/backend/src/presentation/http/accounts/account.controller.test.ts b/apps/backend/src/presentation/http/accounts/account.controller.test.ts index f75f5e07..c7d547b6 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.test.ts @@ -101,6 +101,7 @@ describe('AccountController', () => { describe('create', () => { it('should call accountService.create with correct data', async () => { const requestBody: AccountCreateDTO = { + commodityId: Id.create().valueOf(), currency: Currency.create('USD').valueOf(), description: 'Test Account', initialBalance: Amount.create('1000').valueOf(), diff --git a/apps/backend/src/presentation/http/accounts/account.integration.test.ts b/apps/backend/src/presentation/http/accounts/account.integration.test.ts index c22763d2..ec81c96e 100644 --- a/apps/backend/src/presentation/http/accounts/account.integration.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.integration.test.ts @@ -3,10 +3,11 @@ import { AccountCreateDTO, AccountResponseDTO, AccountTypeValue, + CurrencyCode, UUID, } from '@ledgerly/shared/types'; import { TestDB } from 'src/db/test-db'; -import { Amount } from 'src/domain/domain-core'; +import { Amount, CommodityCode } from 'src/domain/domain-core'; import { Currency } from 'src/domain/domain-core/value-objects/Currency'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { createServer } from 'src/presentation/http'; @@ -16,6 +17,7 @@ const url = `/api${ROUTES.accounts}`; const firstUserAccounts = [ { + commodityId: Id.create().valueOf(), currency: Currency.create('USD').valueOf(), description: 'This is a test account', initialBalance: Amount.create('1000').valueOf(), @@ -23,6 +25,7 @@ const firstUserAccounts = [ type: 'asset' as AccountTypeValue, }, { + commodityId: Id.create().valueOf(), currency: Currency.create('USD').valueOf(), description: 'Savings account for future expenses', initialBalance: Amount.create('1000').valueOf(), @@ -71,13 +74,14 @@ describe('Accounts Integration Tests', () => { const decoded = server.jwt.decode(token) as unknown as { userId: UUID }; userId = Id.restore(decoded.userId).valueOf(); - const testAccounts = getUserTestAccounts( + const testAccountsDTO = getUserTestAccounts( Id.restore(decoded.userId).valueOf(), ); - const promises = testAccounts.map((account) => - testDB.createAccount(userId, account), - ); + const promises = testAccountsDTO.map(async (account) => { + const commodity = await testDB.createCommodity(userId); + return testDB.createAccount(userId, commodity.id, account); + }); accounts = await Promise.all(promises); }); @@ -119,9 +123,14 @@ describe('Accounts Integration Tests', () => { describe('POST /api/accounts', () => { it('should create a new account', async () => { - //TODO: create entity with Account.create - const newAccount = { - currency: 'USD', + const commodity = await testDB.createCommodity(userId, { + code: CommodityCode.create('USD').valueOf(), + name: 'US Dollar', + }); + + const payload: AccountCreateDTO = { + commodityId: commodity.id, + currency: 'USD' as unknown as CurrencyCode, description: 'This is a new account', initialBalance: Amount.create('1000').valueOf(), name: 'New Account', @@ -133,17 +142,17 @@ describe('Accounts Integration Tests', () => { Authorization: `Bearer ${authToken}`, }, method: 'POST', - payload: newAccount, + payload, url, }); const createdAccount = JSON.parse(response.body) as AccountResponseDTO; expect(response.statusCode).toBe(201); - expect(createdAccount.name).toBe(newAccount.name); - expect(createdAccount.currency).toBe(newAccount.currency); - expect(createdAccount.type).toBe(newAccount.type); - expect(createdAccount.description).toBe(newAccount.description); + expect(createdAccount.name).toBe(payload.name); + expect(createdAccount.currency).toBe(payload.currency); + expect(createdAccount.type).toBe(payload.type); + expect(createdAccount.description).toBe(payload.description); const finalResponse = await server.inject({ headers: { diff --git a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts index 87ac3609..e75e8afc 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts @@ -16,6 +16,7 @@ import { } from 'src/application'; import { AccountDbRow, + CommodityDbRow, OperationDbRow, TransactionDbRow, UserDbRow, @@ -45,6 +46,8 @@ describe('Transactions Integration Tests', () => { let authToken: string; let userId: UUID; let user: UserDbRow; + let commodity: CommodityDbRow; + let commodityId: UUID; beforeEach(async () => { testDB = new TestDB(); @@ -55,6 +58,9 @@ describe('Transactions Integration Tests', () => { user = await testDB.createUser(testUser); + commodity = await testDB.createCommodity(user.id); + commodityId = commodity.id; + const token = server.jwt.sign({ email: user.email, userId: user.id, @@ -72,11 +78,11 @@ describe('Transactions Integration Tests', () => { const createAccounts = async () => { return Promise.all([ - testDB.createAccount(userId, { + testDB.createAccount(userId, commodityId, { currency: Currency.create('USD').valueOf(), name: 'Checking USD', }), - testDB.createAccount(userId, { + testDB.createAccount(userId, commodityId, { currency: Currency.create('USD').valueOf(), name: 'Savings USD', }), @@ -146,7 +152,7 @@ describe('Transactions Integration Tests', () => { }); it('should create a multi-currency transaction balanced by value when amounts do not sum to zero', async () => { - const rubAccount = await testDB.createAccount(userId, { + const rubAccount = await testDB.createAccount(userId, commodityId, { currency: Currency.create('RUB').valueOf(), name: 'Cash RUB', }); @@ -372,10 +378,14 @@ describe('Transactions Integration Tests', () => { password: 'password123', }); - const otherUserAccount = await testDB.createAccount(otherUser.id, { - currency: Currency.create('USD').valueOf(), - name: 'Other User Account', - }); + const otherUserAccount = await testDB.createAccount( + otherUser.id, + commodityId, + { + currency: Currency.create('USD').valueOf(), + name: 'Other User Account', + }, + ); const payload: TransactionCreateInput = { currencyCode: Currency.create('USD').valueOf(), @@ -1746,10 +1756,14 @@ describe('Transactions Integration Tests', () => { const otherUser = await testDB.createUser(); - const otherUserAccount = await testDB.createAccount(otherUser.id, { - currency: Currency.create('USD').valueOf(), - name: 'Other User Account', - }); + const otherUserAccount = await testDB.createAccount( + otherUser.id, + commodityId, + { + currency: Currency.create('USD').valueOf(), + name: 'Other User Account', + }, + ); const updatedData = createUpdateRequest({ description: 'Should not be persisted', @@ -1825,10 +1839,14 @@ describe('Transactions Integration Tests', () => { const otherUser = await testDB.createUser(); - const otherUserAccount = await testDB.createAccount(otherUser.id, { - currency: Currency.create('USD').valueOf(), - name: 'Other User Account', - }); + const otherUserAccount = await testDB.createAccount( + otherUser.id, + commodityId, + { + currency: Currency.create('USD').valueOf(), + name: 'Other User Account', + }, + ); const updatedData = createUpdateRequest({ description: 'Should not be persisted', diff --git a/packages/shared/src/types/account.ts b/packages/shared/src/types/account.ts index e94c2e18..74d12d8d 100644 --- a/packages/shared/src/types/account.ts +++ b/packages/shared/src/types/account.ts @@ -9,6 +9,7 @@ export type AccountTypeValue = | "expense"; export type AccountDomain = { + commodityId: UUID; createdAt: IsoDatetimeString; currency: CurrencyCode; currentClearedBalanceLocal: AmountString; @@ -24,6 +25,7 @@ export type AccountDomain = { }; export type AccountCreateDTO = { + commodityId: UUID; currency: CurrencyCode; description: string; initialBalance: AmountString; diff --git a/packages/shared/src/validation/accounts.ts b/packages/shared/src/validation/accounts.ts index e92acc73..9c8d946e 100644 --- a/packages/shared/src/validation/accounts.ts +++ b/packages/shared/src/validation/accounts.ts @@ -14,6 +14,7 @@ import { const accountType = z.enum(ACCOUNT_TYPE_VALUES); export const accountCreateSchema = z.object({ + commodityId: uuid, currency: currencyCode, description: requiredText, initialBalance: amountString, From a1b7df31be7bf04c9b7c9d9cab1a492dc7685b54 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Fri, 17 Jul 2026 12:46:38 +0300 Subject: [PATCH 06/30] LED-66 chore(husky): update pre-commit to include tests --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index c095f3e5..38b5793c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm --filter backend check +pnpm --filter backend check && pnpm --filter backend test \ No newline at end of file From 2c04f6a3510d7c716b4808d47bc16ed147d1ba96 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Fri, 17 Jul 2026 13:06:17 +0300 Subject: [PATCH 07/30] LED-66 feat(accounts): implement commodity schema migration for accounts table --- apps/backend/drizzle/0002_flimsy_menace.sql | 24 + apps/backend/drizzle/meta/0002_snapshot.json | 660 +++++++++++++++++++ apps/backend/drizzle/meta/_journal.json | 7 + 3 files changed, 691 insertions(+) create mode 100644 apps/backend/drizzle/0002_flimsy_menace.sql create mode 100644 apps/backend/drizzle/meta/0002_snapshot.json diff --git a/apps/backend/drizzle/0002_flimsy_menace.sql b/apps/backend/drizzle/0002_flimsy_menace.sql new file mode 100644 index 00000000..ab07b6ff --- /dev/null +++ b/apps/backend/drizzle/0002_flimsy_menace.sql @@ -0,0 +1,24 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_accounts` ( + `commodity_id` text NOT NULL, + `created_at` text NOT NULL, + `currency` 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_system` integer NOT NULL, + `is_tombstone` integer NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `updated_at` text NOT NULL, + `user_id` text NOT NULL, + FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_accounts`("commodity_id", "created_at", "currency", "current_cleared_balance_local", "description", "id", "initial_balance", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id") SELECT "commodity_id", "created_at", "currency", "current_cleared_balance_local", "description", "id", "initial_balance", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id" FROM `accounts`;--> statement-breakpoint +DROP TABLE `accounts`;--> statement-breakpoint +ALTER TABLE `__new_accounts` RENAME TO `accounts`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`); \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0002_snapshot.json b/apps/backend/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..fba90fcb --- /dev/null +++ b/apps/backend/drizzle/meta/0002_snapshot.json @@ -0,0 +1,660 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "be04e3c3-6862-4ae8-93a9-143588adfa38", + "prevId": "694ae242-fadb-4fa3-ad15-1548710dd82a", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "commodity_id": { + "name": "commodity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "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", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initial_balance": { + "name": "initial_balance", + "type": "text", + "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", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_name_unique_idx": { + "name": "user_id_name_unique_idx", + "columns": [ + "user_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "accounts_commodity_id_commodities_id_fk": { + "name": "accounts_commodity_id_commodities_id_fk", + "tableFrom": "accounts", + "tableTo": "commodities", + "columnsFrom": [ + "commodity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transactions": { + "name": "transactions", + "columns": { + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "posting_date": { + "name": "posting_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transaction_date": { + "name": "transaction_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_transactions_user_date": { + "name": "idx_transactions_user_date", + "columns": [ + "user_id", + "transaction_date" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "currencies": { + "name": "currencies", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "operations": { + "name": "operations", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_operations_transaction": { + "name": "idx_operations_transaction", + "columns": [ + "transaction_id" + ], + "isUnique": false + }, + "idx_operations_account": { + "name": "idx_operations_account", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "idx_operations_user": { + "name": "idx_operations_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "operations_account_id_accounts_id_fk": { + "name": "operations_account_id_accounts_id_fk", + "tableFrom": "operations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "operations_transaction_id_transactions_id_fk": { + "name": "operations_transaction_id_transactions_id_fk", + "tableFrom": "operations", + "tableTo": "transactions", + "columnsFrom": [ + "transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "operations_user_id_users_id_fk": { + "name": "operations_user_id_users_id_fk", + "tableFrom": "operations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "base_currency": { + "name": "base_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RUB'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_base_currency_currencies_code_fk": { + "name": "settings_base_currency_currencies_code_fk", + "tableFrom": "settings", + "tableTo": "currencies", + "columnsFrom": [ + "base_currency" + ], + "columnsTo": [ + "code" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "settings_user_id_users_id_fk": { + "name": "settings_user_id_users_id_fk", + "tableFrom": "settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "commodities": { + "name": "commodities", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "precision": { + "name": "precision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "commodities_user_id_code_unique_idx": { + "name": "commodities_user_id_code_unique_idx", + "columns": [ + "user_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "commodities_user_id_users_id_fk": { + "name": "commodities_user_id_users_id_fk", + "tableFrom": "commodities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index 330ef8b0..774b7ad9 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1784193820034, "tag": "0001_small_mister_fear", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1784281745155, + "tag": "0002_flimsy_menace", + "breakpoints": true } ] } \ No newline at end of file From 3150977eb8809fb3fbdfda140c6c8d140cca9905 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Wed, 22 Jul 2026 20:00:21 +0300 Subject: [PATCH 08/30] LED-66 feat(transactions): add commodityId to transaction entity and related functionality --- ...es.sql => 0000_nosy_professor_monster.sql} | 19 + .../drizzle/0001_small_mister_fear.sql | 15 - apps/backend/drizzle/0002_flimsy_menace.sql | 24 - apps/backend/drizzle/meta/0000_snapshot.json | 138 +++- apps/backend/drizzle/meta/0001_snapshot.json | 660 ------------------ apps/backend/drizzle/meta/0002_snapshot.json | 660 ------------------ apps/backend/drizzle/meta/_journal.json | 18 +- .../mappers/operation.mapper.test.ts | 5 +- .../mappers/transaction.mapper.test.ts | 4 +- .../application/mappers/transaction.mapper.ts | 1 + .../usecases/transaction/CreateTransaction.ts | 15 +- .../__tests__/createTransaction.test.ts | 28 +- .../__tests__/deleteTransaction.test.ts | 4 +- .../__tests__/updateTransaction.test.ts | 4 +- apps/backend/src/db/schemas/transactions.ts | 6 +- apps/backend/src/db/test-db.ts | 27 +- .../backend/src/db/test-utils/builder.test.ts | 26 +- .../src/db/test-utils/prettyPrint.test.ts | 21 +- .../src/db/test-utils/testEntityBuilder.ts | 36 + apps/backend/src/di/container.ts | 1 + .../domain/domain-entity-identity-api.test.ts | 1 + .../operations/operation.entity.test.ts | 4 +- .../transactions/transaction.entity.test.ts | 204 ++++-- .../domain/transactions/transaction.entity.ts | 23 +- apps/backend/src/domain/transactions/types.ts | 2 + .../operation-persistence.mapper.test.ts | 5 +- .../operations/operation.repository.test.ts | 21 +- .../transaction-persistence.mapper.test.ts | 7 +- .../transaction-persistence.mapper.ts | 1 + .../transaction-query.repository.test.ts | 11 +- .../transaction.repository.test.ts | 30 +- .../db/transaction/transaction.repository.ts | 27 +- .../transaction.controller.test.ts | 1 + .../transaction.integration.test.ts | 212 ++++-- packages/shared/src/types/transaction.ts | 1 + .../shared/src/validation/transactions.ts | 1 + 36 files changed, 728 insertions(+), 1535 deletions(-) rename apps/backend/drizzle/{0000_keen_gertrude_yorkes.sql => 0000_nosy_professor_monster.sql} (78%) delete mode 100644 apps/backend/drizzle/0001_small_mister_fear.sql delete mode 100644 apps/backend/drizzle/0002_flimsy_menace.sql delete mode 100644 apps/backend/drizzle/meta/0001_snapshot.json delete mode 100644 apps/backend/drizzle/meta/0002_snapshot.json diff --git a/apps/backend/drizzle/0000_keen_gertrude_yorkes.sql b/apps/backend/drizzle/0000_nosy_professor_monster.sql similarity index 78% rename from apps/backend/drizzle/0000_keen_gertrude_yorkes.sql rename to apps/backend/drizzle/0000_nosy_professor_monster.sql index 2af9b67c..08394aed 100644 --- a/apps/backend/drizzle/0000_keen_gertrude_yorkes.sql +++ b/apps/backend/drizzle/0000_nosy_professor_monster.sql @@ -1,4 +1,5 @@ CREATE TABLE `accounts` ( + `commodity_id` text NOT NULL, `created_at` text NOT NULL, `currency` text NOT NULL, `current_cleared_balance_local` text NOT NULL, @@ -11,11 +12,13 @@ CREATE TABLE `accounts` ( `type` text NOT NULL, `updated_at` text NOT NULL, `user_id` text NOT NULL, + FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`);--> statement-breakpoint CREATE TABLE `transactions` ( + `valuation_commodity_id` text NOT NULL, `created_at` text NOT NULL, `currency` text NOT NULL, `description` text NOT NULL, @@ -26,6 +29,7 @@ CREATE TABLE `transactions` ( `updated_at` text NOT NULL, `user_id` text NOT NULL, `version` integer NOT NULL, + FOREIGN KEY (`valuation_commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint @@ -74,3 +78,18 @@ CREATE TABLE `settings` ( FOREIGN KEY (`base_currency`) REFERENCES `currencies`(`code`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action ); +--> statement-breakpoint +CREATE TABLE `commodities` ( + `code` text NOT NULL, + `created_at` text NOT NULL, + `id` text PRIMARY KEY NOT NULL, + `is_tombstone` integer NOT NULL, + `name` text NOT NULL, + `precision` integer NOT NULL, + `symbol` text, + `updated_at` text NOT NULL, + `user_id` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `commodities_user_id_code_unique_idx` ON `commodities` (`user_id`,`code`); \ No newline at end of file diff --git a/apps/backend/drizzle/0001_small_mister_fear.sql b/apps/backend/drizzle/0001_small_mister_fear.sql deleted file mode 100644 index 1baf91b0..00000000 --- a/apps/backend/drizzle/0001_small_mister_fear.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE `commodities` ( - `code` text NOT NULL, - `created_at` text NOT NULL, - `id` text PRIMARY KEY NOT NULL, - `is_tombstone` integer NOT NULL, - `name` text NOT NULL, - `precision` integer NOT NULL, - `symbol` text, - `updated_at` text NOT NULL, - `user_id` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `commodities_user_id_code_unique_idx` ON `commodities` (`user_id`,`code`);--> statement-breakpoint -ALTER TABLE `accounts` ADD `commodity_id` text NOT NULL REFERENCES commodities(id); \ No newline at end of file diff --git a/apps/backend/drizzle/0002_flimsy_menace.sql b/apps/backend/drizzle/0002_flimsy_menace.sql deleted file mode 100644 index ab07b6ff..00000000 --- a/apps/backend/drizzle/0002_flimsy_menace.sql +++ /dev/null @@ -1,24 +0,0 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_accounts` ( - `commodity_id` text NOT NULL, - `created_at` text NOT NULL, - `currency` 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_system` integer NOT NULL, - `is_tombstone` integer NOT NULL, - `name` text NOT NULL, - `type` text NOT NULL, - `updated_at` text NOT NULL, - `user_id` text NOT NULL, - FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -INSERT INTO `__new_accounts`("commodity_id", "created_at", "currency", "current_cleared_balance_local", "description", "id", "initial_balance", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id") SELECT "commodity_id", "created_at", "currency", "current_cleared_balance_local", "description", "id", "initial_balance", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id" FROM `accounts`;--> statement-breakpoint -DROP TABLE `accounts`;--> statement-breakpoint -ALTER TABLE `__new_accounts` RENAME TO `accounts`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`); \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/meta/0000_snapshot.json index a1a0db25..023c5227 100644 --- a/apps/backend/drizzle/meta/0000_snapshot.json +++ b/apps/backend/drizzle/meta/0000_snapshot.json @@ -1,12 +1,19 @@ { "version": "6", "dialect": "sqlite", - "id": "b3626041-a73e-4294-b580-3c53764e0f43", + "id": "67f3394c-dc2e-4f8d-95c2-f74c1aef120c", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "accounts": { "name": "accounts", "columns": { + "commodity_id": { + "name": "commodity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "text", @@ -103,6 +110,19 @@ } }, "foreignKeys": { + "accounts_commodity_id_commodities_id_fk": { + "name": "accounts_commodity_id_commodities_id_fk", + "tableFrom": "accounts", + "tableTo": "commodities", + "columnsFrom": [ + "commodity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, "accounts_user_id_users_id_fk": { "name": "accounts_user_id_users_id_fk", "tableFrom": "accounts", @@ -124,6 +144,13 @@ "transactions": { "name": "transactions", "columns": { + "valuation_commodity_id": { + "name": "valuation_commodity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "text", @@ -206,6 +233,19 @@ } }, "foreignKeys": { + "transactions_valuation_commodity_id_commodities_id_fk": { + "name": "transactions_valuation_commodity_id_commodities_id_fk", + "tableFrom": "transactions", + "tableTo": "commodities", + "columnsFrom": [ + "valuation_commodity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, "transactions_user_id_users_id_fk": { "name": "transactions_user_id_users_id_fk", "tableFrom": "transactions", @@ -529,6 +569,102 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} + }, + "commodities": { + "name": "commodities", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "precision": { + "name": "precision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "commodities_user_id_code_unique_idx": { + "name": "commodities_user_id_code_unique_idx", + "columns": [ + "user_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "commodities_user_id_users_id_fk": { + "name": "commodities_user_id_users_id_fk", + "tableFrom": "commodities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} } }, "views": {}, diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 48a15077..00000000 --- a/apps/backend/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,660 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "694ae242-fadb-4fa3-ad15-1548710dd82a", - "prevId": "b3626041-a73e-4294-b580-3c53764e0f43", - "tables": { - "accounts": { - "name": "accounts", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "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", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "initial_balance": { - "name": "initial_balance", - "type": "text", - "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", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_id_name_unique_idx": { - "name": "user_id_name_unique_idx", - "columns": [ - "user_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "accounts_commodity_id_commodities_id_fk": { - "name": "accounts_commodity_id_commodities_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "transactions": { - "name": "transactions", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "posting_date": { - "name": "posting_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_date": { - "name": "transaction_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_transactions_user_date": { - "name": "idx_transactions_user_date", - "columns": [ - "user_id", - "transaction_date" - ], - "isUnique": false - } - }, - "foreignKeys": { - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", - "tableFrom": "transactions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "currencies": { - "name": "currencies", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "operations": { - "name": "operations", - "columns": { - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "amount": { - "name": "amount", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_id": { - "name": "transaction_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_operations_transaction": { - "name": "idx_operations_transaction", - "columns": [ - "transaction_id" - ], - "isUnique": false - }, - "idx_operations_account": { - "name": "idx_operations_account", - "columns": [ - "account_id" - ], - "isUnique": false - }, - "idx_operations_user": { - "name": "idx_operations_user", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", - "tableFrom": "operations", - "tableTo": "accounts", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", - "tableFrom": "operations", - "tableTo": "transactions", - "columnsFrom": [ - "transaction_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", - "tableFrom": "operations", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "settings": { - "name": "settings", - "columns": { - "base_currency": { - "name": "base_currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'RUB'" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "settings_base_currency_currencies_code_fk": { - "name": "settings_base_currency_currencies_code_fk", - "tableFrom": "settings", - "tableTo": "currencies", - "columnsFrom": [ - "base_currency" - ], - "columnsTo": [ - "code" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "settings_user_id_users_id_fk": { - "name": "settings_user_id_users_id_fk", - "tableFrom": "settings", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "commodities": { - "name": "commodities", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "precision": { - "name": "precision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "commodities_user_id_code_unique_idx": { - "name": "commodities_user_id_code_unique_idx", - "columns": [ - "user_id", - "code" - ], - "isUnique": true - } - }, - "foreignKeys": { - "commodities_user_id_users_id_fk": { - "name": "commodities_user_id_users_id_fk", - "tableFrom": "commodities", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0002_snapshot.json b/apps/backend/drizzle/meta/0002_snapshot.json deleted file mode 100644 index fba90fcb..00000000 --- a/apps/backend/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,660 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "be04e3c3-6862-4ae8-93a9-143588adfa38", - "prevId": "694ae242-fadb-4fa3-ad15-1548710dd82a", - "tables": { - "accounts": { - "name": "accounts", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "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", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "initial_balance": { - "name": "initial_balance", - "type": "text", - "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", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_id_name_unique_idx": { - "name": "user_id_name_unique_idx", - "columns": [ - "user_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "accounts_commodity_id_commodities_id_fk": { - "name": "accounts_commodity_id_commodities_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "transactions": { - "name": "transactions", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "posting_date": { - "name": "posting_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_date": { - "name": "transaction_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_transactions_user_date": { - "name": "idx_transactions_user_date", - "columns": [ - "user_id", - "transaction_date" - ], - "isUnique": false - } - }, - "foreignKeys": { - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", - "tableFrom": "transactions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "currencies": { - "name": "currencies", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "operations": { - "name": "operations", - "columns": { - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "amount": { - "name": "amount", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_id": { - "name": "transaction_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_operations_transaction": { - "name": "idx_operations_transaction", - "columns": [ - "transaction_id" - ], - "isUnique": false - }, - "idx_operations_account": { - "name": "idx_operations_account", - "columns": [ - "account_id" - ], - "isUnique": false - }, - "idx_operations_user": { - "name": "idx_operations_user", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", - "tableFrom": "operations", - "tableTo": "accounts", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", - "tableFrom": "operations", - "tableTo": "transactions", - "columnsFrom": [ - "transaction_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", - "tableFrom": "operations", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "settings": { - "name": "settings", - "columns": { - "base_currency": { - "name": "base_currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'RUB'" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "settings_base_currency_currencies_code_fk": { - "name": "settings_base_currency_currencies_code_fk", - "tableFrom": "settings", - "tableTo": "currencies", - "columnsFrom": [ - "base_currency" - ], - "columnsTo": [ - "code" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "settings_user_id_users_id_fk": { - "name": "settings_user_id_users_id_fk", - "tableFrom": "settings", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "commodities": { - "name": "commodities", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "precision": { - "name": "precision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "commodities_user_id_code_unique_idx": { - "name": "commodities_user_id_code_unique_idx", - "columns": [ - "user_id", - "code" - ], - "isUnique": true - } - }, - "foreignKeys": { - "commodities_user_id_users_id_fk": { - "name": "commodities_user_id_users_id_fk", - "tableFrom": "commodities", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index 774b7ad9..d223fcf6 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -5,22 +5,8 @@ { "idx": 0, "version": "6", - "when": 1776091943739, - "tag": "0000_keen_gertrude_yorkes", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1784193820034, - "tag": "0001_small_mister_fear", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1784281745155, - "tag": "0002_flimsy_menace", + "when": 1784739467705, + "tag": "0000_nosy_professor_monster", "breakpoints": true } ] diff --git a/apps/backend/src/application/mappers/operation.mapper.test.ts b/apps/backend/src/application/mappers/operation.mapper.test.ts index 2ef690c9..6ac3c9ed 100644 --- a/apps/backend/src/application/mappers/operation.mapper.test.ts +++ b/apps/backend/src/application/mappers/operation.mapper.test.ts @@ -3,7 +3,7 @@ import { CreateOperationRequestDTO, UpdateOperationRequestDTO, } from 'src/application/dto'; -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { User } from 'src/domain'; import { @@ -25,8 +25,11 @@ describe('OperationMapper', () => { beforeAll(async () => { user = await createUser(); + const commodity = createCommodity(user, { code: 'USD' }); + const fixture = TransactionBuilder.request({ accounts: ['USD'], + commodity, operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'USD', amount: '-100', description: 'Credit' }, diff --git a/apps/backend/src/application/mappers/transaction.mapper.test.ts b/apps/backend/src/application/mappers/transaction.mapper.test.ts index 47f1d79b..bed5cbe9 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.test.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.test.ts @@ -1,4 +1,4 @@ -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { Transaction } from 'src/domain'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,9 +10,11 @@ describe('TransactionMapper', () => { beforeAll(async () => { const user = await createUser(); + const commodity = createCommodity(user, { code: 'USD' }); fixture = TransactionBuilder.transaction({ accounts: ['USD'], + commodity, operations: [ { accountKey: 'USD', diff --git a/apps/backend/src/application/mappers/transaction.mapper.ts b/apps/backend/src/application/mappers/transaction.mapper.ts index 9e9ddfec..9cbf5a25 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.ts @@ -40,6 +40,7 @@ export class TransactionMapper { ); return { + commodityId: dto.commodityId, currency: Currency.create(dto.currencyCode), description: dto.description, operations, diff --git a/apps/backend/src/application/usecases/transaction/CreateTransaction.ts b/apps/backend/src/application/usecases/transaction/CreateTransaction.ts index f8759469..e969aaf0 100644 --- a/apps/backend/src/application/usecases/transaction/CreateTransaction.ts +++ b/apps/backend/src/application/usecases/transaction/CreateTransaction.ts @@ -3,12 +3,13 @@ import { TransactionResponseDTO, } from 'src/application/dto'; import type { + CommodityRepositoryInterface, TransactionManagerInterface, TransactionRepositoryInterface, } from 'src/application/interfaces'; import { TransactionMapper } from 'src/application/mappers'; import { TransactionContextLoader } from 'src/application/services/TransactionService'; -import { User } from 'src/domain'; +import { Commodity, User } from 'src/domain'; import { Transaction } from 'src/domain/transactions'; export class CreateTransactionUseCase { @@ -16,16 +17,23 @@ export class CreateTransactionUseCase { protected readonly transactionManager: TransactionManagerInterface, protected readonly transactionRepository: TransactionRepositoryInterface, protected readonly transactionContextLoader: TransactionContextLoader, + protected readonly commodityRepository: CommodityRepositoryInterface, ) {} async execute( user: User, data: CreateTransactionRequestDTO, ): Promise { + const { commodityId, operations } = data; const createdTransaction = await this.transactionManager.run(async () => { + const commoditySnapshot = await this.commodityRepository.getById( + user.getId().valueOf(), + commodityId, + ); + const context = await this.transactionContextLoader.loadContext( user, - data.operations, + operations, ); const createTransactionProps = TransactionMapper.toCreateTransactionProps( @@ -33,8 +41,11 @@ export class CreateTransactionUseCase { context, ); + const commodity = Commodity.restore(commoditySnapshot); + const transaction = Transaction.create( user.getId(), + commodity, createTransactionProps, ); diff --git a/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts index 22cca41a..91367344 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts @@ -1,11 +1,14 @@ import type { + CommodityRepositoryInterface, TransactionManagerInterface, TransactionRepositoryInterface, } from 'src/application/interfaces'; import { TransactionContextLoader } from 'src/application/services/TransactionService'; import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils/testEntityBuilder'; -import { User, Transaction } from 'src/domain'; +import { User, Transaction, Commodity } from 'src/domain'; +import { CommodityCode } from 'src/domain/domain-core/value-objects/CommodityCode'; +import { Name } from 'src/domain/domain-core/value-objects/Name'; import { UnbalancedTransactionError } from 'src/domain/domain.errors'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -13,6 +16,7 @@ import { CreateTransactionUseCase } from '../CreateTransaction'; describe('CreateTransactionUseCase', () => { let user: User; + let commodity: Commodity; const mockTransactionRepository = { create: vi.fn(), @@ -26,10 +30,15 @@ describe('CreateTransactionUseCase', () => { loadContext: vi.fn(), }; + const commodityRepository = { + getById: vi.fn(), + }; + const createTransactionUseCase = new CreateTransactionUseCase( transactionManager as unknown as TransactionManagerInterface, mockTransactionRepository as unknown as TransactionRepositoryInterface, transactionContextLoader as unknown as TransactionContextLoader, + commodityRepository as unknown as CommodityRepositoryInterface, ); const transactionData = { @@ -46,10 +55,21 @@ describe('CreateTransactionUseCase', () => { beforeAll(async () => { user = await createUser(); + + commodity = Commodity.create( + user, + Name.create('Test Commodity'), + CommodityCode.create('TEST'), + 2, + null, + ); }); beforeEach(() => { - vi.clearAllMocks(); + vi.resetAllMocks(); + + transactionManager.run.mockImplementation((cb: () => unknown) => cb()); + commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); }); describe('execute', () => { @@ -57,6 +77,7 @@ describe('CreateTransactionUseCase', () => { const { transactionContext, transactionDTO } = TransactionBuilder.request( { accounts: ['USD'], + commodity, operations: operationsData, settings: transactionData, user, @@ -135,6 +156,7 @@ describe('CreateTransactionUseCase', () => { const { transactionDTO } = TransactionBuilder.request({ accounts: ['USD'], + commodity, operations: [], settings: transactionData, user, @@ -152,6 +174,7 @@ describe('CreateTransactionUseCase', () => { const { transactionContext, transactionDTO } = TransactionBuilder.request( { accounts: ['USD'], + commodity, operations: operationsData, settings: transactionData, user, @@ -176,6 +199,7 @@ describe('CreateTransactionUseCase', () => { const { transactionContext, transactionDTO } = TransactionBuilder.request( { accounts: ['USD'], + commodity, operations: unbalancedOperations, settings: transactionData, user, diff --git a/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts index ab30211c..89ce82e9 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts @@ -7,7 +7,7 @@ import type { TransactionManagerInterface, TransactionRepositoryInterface, } from 'src/application/interfaces'; -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils/testEntityBuilder'; import { Transaction, User } from 'src/domain'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -41,9 +41,11 @@ describe('DeleteTransactionUseCase', () => { beforeEach(() => { vi.clearAllMocks(); + const commodity = createCommodity(user); const data = TransactionBuilder.transaction({ accounts: ['USD', 'EUR'], + commodity, operations: [ { accountKey: 'USD', diff --git a/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts index 3737a969..4f02c03d 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts @@ -15,7 +15,7 @@ import type { TransactionRepositoryInterface, } from 'src/application/interfaces'; import { TransactionContextLoader } from 'src/application/services/TransactionService/transaction.context-loader'; -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { compareEntities, TransactionBuilder } from 'src/db/test-utils'; import { Transaction, User } from 'src/domain'; import { Amount, Id, Version } from 'src/domain/domain-core'; @@ -145,9 +145,11 @@ describe('UpdateTransactionUseCase', () => { beforeEach(() => { vi.clearAllMocks(); + const commodity = createCommodity(user); const data = TransactionBuilder.transaction({ accounts: ['USD', 'EUR'], + commodity, operations: [ { accountKey: 'USD', diff --git a/apps/backend/src/db/schemas/transactions.ts b/apps/backend/src/db/schemas/transactions.ts index b4a06b98..3c3b0f04 100644 --- a/apps/backend/src/db/schemas/transactions.ts +++ b/apps/backend/src/db/schemas/transactions.ts @@ -2,7 +2,7 @@ import { CurrencyCode, UUID } from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm'; import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'; -import { OperationDbRow, operationsTable } from '../schema'; +import { commoditiesTable, OperationDbRow, operationsTable } from '../schema'; import { createdAt, @@ -18,6 +18,10 @@ import { usersTable } from './users'; export const transactionsTable = sqliteTable( 'transactions', { + commodityId: text('valuation_commodity_id') + .notNull() + .references(() => commoditiesTable.id) + .$type(), createdAt, currency: text('currency').notNull().$type(), description, diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index 040c9cff..c91d9823 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -28,6 +28,7 @@ import { import { OperationSnapshot } from 'src/domain/operations/types'; import { TransactionSnapshot } from 'src/domain/transactions/types'; import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; +import { CommodityPersistenceMapper } from 'src/infrastructure/db/commodities/commodity-persistence.mapper'; import { AmountFormatter } from 'src/presentation/formatters'; import { @@ -222,6 +223,7 @@ export class TestDB { createTransaction = async ( userId: UUID, + commodityId: UUID, params?: CreateTransactionProps, ): Promise => { const transactionData: TransactionDbInsert = { @@ -235,6 +237,7 @@ export class TestDB { postingDate: params?.postingDate ?? DateValue.create().valueOf(), transactionDate: params?.transactionDate ?? DateValue.create().valueOf(), ...params, + commodityId, userId, version: 0, }; @@ -250,6 +253,7 @@ export class TestDB { createTransactionWithOperations = async ( userId: UUID, + commodityId: UUID, params?: { description?: string; postingDate?: IsoDateString; @@ -268,7 +272,11 @@ export class TestDB { }[]; }, ): Promise => { - const transaction = await this.createTransaction(userId, params); + const transaction = await this.createTransaction( + userId, + commodityId, + params, + ); const operations: OperationDbRow[] = []; @@ -286,6 +294,7 @@ export class TestDB { createTransactionFromSeed = async ( userId: UUID, + commodityId: UUID, { currencyCode = 'USD' as CurrencyCode, description, @@ -295,7 +304,7 @@ export class TestDB { transactionDate = '2023-01-01' as IsoDateString, }: TransactionSeed, ): Promise => { - return this.createTransactionWithOperations(userId, { + return this.createTransactionWithOperations(userId, commodityId, { currencyCode, description, isTombstone, @@ -605,6 +614,10 @@ export class TestDB { password: 'hashed_password', })); + const transactionCommodity = CommodityPersistenceMapper.toDomain( + await this.createCommodity(user.id), + ); + const usdCommodity = await this.createCommodity(user.id, { code: CommodityCode.create('USD').valueOf(), name: 'US Dollar', @@ -634,8 +647,14 @@ export class TestDB { name: 'Credit Card EUR', }); - const transaction1 = await this.createTransaction(user.id); - const transaction2 = await this.createTransaction(user.id); + const transaction1 = await this.createTransaction( + user.id, + transactionCommodity.getId().valueOf(), + ); + const transaction2 = await this.createTransaction( + user.id, + transactionCommodity.getId().valueOf(), + ); await this.createOperation(user.id, { accountId: accountUSD1.id, diff --git a/apps/backend/src/db/test-utils/builder.test.ts b/apps/backend/src/db/test-utils/builder.test.ts index 6ed47b29..5b758ac6 100644 --- a/apps/backend/src/db/test-utils/builder.test.ts +++ b/apps/backend/src/db/test-utils/builder.test.ts @@ -1,14 +1,23 @@ -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; +import { Commodity, User } from 'src/domain'; import { InsufficientOperationsError } from 'src/domain/domain.errors'; -import { describe, expect, it } from 'vitest'; +import { beforeAll, describe, expect, it } from 'vitest'; import { TransactionBuilder } from './testEntityBuilder'; describe('TransactionBuilder', () => { - it('should be implemented', async () => { - const user = await createUser(); + let user: User; + let commodity: Commodity; + + beforeAll(async () => { + user = await createUser(); + commodity = createCommodity(user); + }); + + it('should be implemented', () => { TransactionBuilder.transaction({ accounts: ['USD', 'EUR'], + commodity: commodity, operations: [ { accountKey: 'USD', amount: '10000', description: '1' }, { @@ -21,10 +30,10 @@ describe('TransactionBuilder', () => { }); }); - it('builds an invalid request fixture without creating a domain transaction', async () => { - const user = await createUser(); + it('builds an invalid request fixture without creating a domain transaction', () => { const fixture = TransactionBuilder.request({ accounts: ['USD'], + commodity, operations: [{ accountKey: 'USD', amount: '0' }], user, }); @@ -32,12 +41,11 @@ describe('TransactionBuilder', () => { expect(fixture.transactionDTO.operations).toHaveLength(1); }); - it('applies domain invariants when building a transaction', async () => { - const user = await createUser(); - + it('applies domain invariants when building a transaction', () => { expect(() => TransactionBuilder.transaction({ accounts: ['USD'], + commodity, operations: [{ accountKey: 'USD', amount: '0' }], user, }), diff --git a/apps/backend/src/db/test-utils/prettyPrint.test.ts b/apps/backend/src/db/test-utils/prettyPrint.test.ts index ca161798..572206fe 100644 --- a/apps/backend/src/db/test-utils/prettyPrint.test.ts +++ b/apps/backend/src/db/test-utils/prettyPrint.test.ts @@ -1,18 +1,27 @@ -import { createUser } from 'src/db/createTestUser'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { createCommodity, createUser } from 'src/db/createTestUser'; +import { Commodity, User } from 'src/domain'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { prettyPrint } from './prettyPrint'; import { TransactionBuilder } from './testEntityBuilder'; describe('prettyPrint', () => { + let user: User; + let commodity: Commodity; + afterEach(() => { vi.restoreAllMocks(); }); - it('prints an operation in PTA format', async () => { - const user = await createUser(); + beforeAll(async () => { + user = await createUser(); + commodity = createCommodity(user); + }); + + it('prints an operation in PTA format', () => { const data = TransactionBuilder.transaction({ accounts: ['USD'], + commodity, operations: [ { accountKey: 'USD', @@ -43,10 +52,10 @@ describe('prettyPrint', () => { ); }); - it('prints a transaction in PTA format', async () => { - const user = await createUser(); + it('prints a transaction in PTA format', () => { const data = TransactionBuilder.transaction({ accounts: ['USD', 'EUR'], + commodity: commodity, operations: [ { accountKey: 'USD', diff --git a/apps/backend/src/db/test-utils/testEntityBuilder.ts b/apps/backend/src/db/test-utils/testEntityBuilder.ts index 8f9f9378..90079a9d 100644 --- a/apps/backend/src/db/test-utils/testEntityBuilder.ts +++ b/apps/backend/src/db/test-utils/testEntityBuilder.ts @@ -47,6 +47,7 @@ export type TransactionProps = { export type TransactionBuilderOptions = { user: User; + commodity: Commodity; accounts: string[]; operations: OperationDataForTransaction[]; settings?: Partial; @@ -68,6 +69,7 @@ export type TransactionRequestBuilderResult = { postingDate: IsoDateString; transactionDate: IsoDateString; userId: UUID; + commodityId: UUID; }; operationsData: OperationDataForTransaction[]; }; @@ -92,21 +94,33 @@ export class TransactionBuilder { private readonly transactionDate: IsoDateString; private readonly description: string; private readonly transactionCurrency: Currency; + private readonly transactionCommodity: Commodity; private constructor(options: TransactionBuilderOptions) { this.user = options.user; + this.operationsData = [...options.operations]; + this.postingDate = DateValue.restore( options.settings?.postingDate ?? '2023-01-01', ).valueOf(); + this.transactionDate = DateValue.restore( options.settings?.transactionDate ?? '2023-01-01', ).valueOf(); + this.description = options.settings?.description ?? 'Test Transaction'; + this.transactionCurrency = Currency.create( options.settings?.currencyCode ?? 'USD', ); + this.transactionCommodity = Commodity.create( + this.user, + Name.create(`Commodity ${options.settings?.currencyCode ?? 'USD'}`), + CommodityCode.create(options.settings?.currencyCode ?? 'USD'), + ); + this.createAccounts(options.accounts); } @@ -131,6 +145,7 @@ export class TransactionBuilder { private createAccounts(currencyCodes: string[]): void { currencyCodes.forEach((currencyCode) => { const code = CommodityCode.create(currencyCode); + const commodity = Commodity.create( this.user, Name.create(`Commodity ${currencyCode}`), @@ -233,6 +248,11 @@ export class TransactionBuilder { private buildTransactionProps(): CreateTransactionProps { return { + commodityId: this.getCommodityByCurrency( + this.transactionCurrency.valueOf(), + ) + .getId() + .valueOf(), currency: this.transactionCurrency, description: this.description, operations: this.operationsData.map((operation) => { @@ -265,12 +285,22 @@ export class TransactionBuilder { systemAccountsMap: this.systemAccounts, }, transactionData: { + commodityId: this.getCommodityByCurrency( + this.transactionCurrency.valueOf(), + ) + .getId() + .valueOf(), description: this.description, postingDate: this.postingDate, transactionDate: this.transactionDate, userId: this.user.getId().valueOf(), }, transactionDTO: { + commodityId: this.getCommodityByCurrency( + this.transactionCurrency.valueOf(), + ) + .getId() + .valueOf(), currencyCode: this.transactionCurrency.valueOf(), description: this.description, operations: this.buildOperationsDTO(), @@ -283,8 +313,14 @@ export class TransactionBuilder { private buildTransaction(): TransactionBuilderResult { const requestFixture = this.buildRequest(); + + const commodity = this.getCommodityByCurrency( + requestFixture.transactionDTO.currencyCode, + ); + const transaction = Transaction.create( this.user.getId(), + commodity, this.buildTransactionProps(), ); diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index fddd1558..eb9d81f0 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -90,6 +90,7 @@ export const createContainer = (db: DataBase): AppContainer => { transactionManager, transactionRepository, transactionContextLoader, + commodityRepository, ); const getTransactionByIdUseCase = new GetTransactionByIdUseCase( diff --git a/apps/backend/src/domain/domain-entity-identity-api.test.ts b/apps/backend/src/domain/domain-entity-identity-api.test.ts index 06894812..968c3b50 100644 --- a/apps/backend/src/domain/domain-entity-identity-api.test.ts +++ b/apps/backend/src/domain/domain-entity-identity-api.test.ts @@ -17,6 +17,7 @@ describe('domain entity identity API conventions', () => { const user = await createUser(); const account = createAccount(user); const transaction = Transaction.restore({ + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), currency: Currency.create('USD').valueOf(), description: 'Restored transaction', diff --git a/apps/backend/src/domain/operations/operation.entity.test.ts b/apps/backend/src/domain/operations/operation.entity.test.ts index 0dc33ec5..351dcf7e 100644 --- a/apps/backend/src/domain/operations/operation.entity.test.ts +++ b/apps/backend/src/domain/operations/operation.entity.test.ts @@ -1,5 +1,5 @@ import { apiErrorCodes } from '@ledgerly/shared/types'; -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { compareEntities, TransactionBuilder } from 'src/db/test-utils'; import { TransactionProps } from 'src/db/test-utils/testEntityBuilder'; import { @@ -48,9 +48,11 @@ describe('Operation Domain Entity', () => { beforeAll(async () => { user = await createUser(); userId = user.getId(); + const commodity = createCommodity(user, { code: 'USD' }); const data = TransactionBuilder.transaction({ accounts: ['USD', 'EUR'], + commodity, operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'EUR', amount: '-100', description: 'Credit' }, diff --git a/apps/backend/src/domain/transactions/transaction.entity.test.ts b/apps/backend/src/domain/transactions/transaction.entity.test.ts index 95bb3947..616dbfe0 100644 --- a/apps/backend/src/domain/transactions/transaction.entity.test.ts +++ b/apps/backend/src/domain/transactions/transaction.entity.test.ts @@ -1,5 +1,5 @@ import { apiErrorCodes } from '@ledgerly/shared/types'; -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { compareEntities, TransactionBuilder, @@ -31,6 +31,7 @@ import { } from 'vitest'; import { Account } from '../accounts'; +import { Commodity } from '../commodities'; import { Amount, Currency, DateValue, Id, Version } from '../domain-core'; import { CreateOperationProps, @@ -91,6 +92,7 @@ const toCreateTransactionProps = ( 'getAccountByKey' | 'operationsData' | 'transactionDTO' | 'transactionData' >, ): CreateTransactionProps => ({ + commodityId: fixture.transactionData.commodityId, currency: Currency.create(fixture.transactionDTO.currencyCode), description: fixture.transactionData.description, operations: toCreateOperationProps( @@ -115,6 +117,7 @@ const toMetadataUpdateData = ( describe('Transaction Domain Entity', () => { let user: User; + let commodity: Commodity; let data: TransactionBuilderResult; @@ -191,8 +194,11 @@ describe('Transaction Domain Entity', () => { beforeAll(async () => { user = await createUser(); + commodity = createCommodity(user, { code: 'USD' }); + data = TransactionBuilder.transaction({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: operationsData, settings: transactionRawData, user, @@ -213,7 +219,11 @@ describe('Transaction Domain Entity', () => { it('should create a valid transaction with operation', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); expect(transaction).toBeInstanceOf(Transaction); expect(transaction.description).toBe(transactionData.description); @@ -241,7 +251,11 @@ describe('Transaction Domain Entity', () => { }); it('should serialize and deserialize correctly', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const transactionSnapshot = transaction.toSnapshot(); @@ -257,7 +271,11 @@ describe('Transaction Domain Entity', () => { }); it('should restore the persisted version as a value object', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const transactionSnapshot = transaction.toSnapshot(); const restoredTransaction = Transaction.restore({ @@ -272,7 +290,11 @@ describe('Transaction Domain Entity', () => { }); it('should restore tombstone operations without allowing them to be updated', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const transactionSnapshot = transaction.toSnapshot(); const [operationToDelete1, operationToDelete2, ...activeOperations] = transactionSnapshot.operations; @@ -336,7 +358,11 @@ describe('Transaction Domain Entity', () => { }); it('should expose full and active snapshots explicitly', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const transactionSnapshot = transaction.toSnapshot(); const operationToDelete = transactionSnapshot.operations.find( @@ -393,7 +419,11 @@ describe('Transaction Domain Entity', () => { describe('Updating Transaction data', () => { it('Should update only description, increment version and update timestamps', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const description = 'Updated transaction description'; const originalSnapshot = transaction.toSnapshot(); @@ -428,7 +458,11 @@ describe('Transaction Domain Entity', () => { }); it('Should update only postingDate, increment version and update timestamps', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const postingDate = DateValue.restore('2024-02-15').valueOf(); @@ -459,7 +493,11 @@ describe('Transaction Domain Entity', () => { }); it('Should update only transactionDate, increment version and update timestamps', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const transactionDate = DateValue.restore('2024-03-10').valueOf(); @@ -487,7 +525,11 @@ describe('Transaction Domain Entity', () => { }); it('Should update all fields at once, increment version and update timestamps', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); @@ -521,7 +563,11 @@ describe('Transaction Domain Entity', () => { describe('Manage Operations', () => { it('rejects attaching an operation that is already attached', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const operation = transaction.getOperations()[0]; const error = captureThrownError(() => @@ -539,10 +585,15 @@ describe('Transaction Domain Entity', () => { }); it('rejects an operation that belongs to another transaction of the same user', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const otherTransaction = Transaction.create( user.getId(), + commodity, transactionData, ); const foreignOperation = otherTransaction.getOperations()[0]; @@ -596,7 +647,11 @@ describe('Transaction Domain Entity', () => { }; it('should reject the same operation ID in update and delete when represented by different Id instances', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const operationId = transaction.getOperations()[0].getId().valueOf(); const error = captureConflictingOperationIdsError(() => @@ -620,7 +675,11 @@ describe('Transaction Domain Entity', () => { }); it('should reject duplicate update IDs represented by different Id instances', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const operationId = transaction.getOperations()[0].getId().valueOf(); const error = captureConflictingOperationIdsError(() => @@ -646,7 +705,11 @@ describe('Transaction Domain Entity', () => { }); it('should reject duplicate delete IDs represented by different Id instances', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const operationId = transaction.getOperations()[0].getId().valueOf(); const error = captureConflictingOperationIdsError(() => @@ -669,7 +732,11 @@ describe('Transaction Domain Entity', () => { }); it('should reject updating a non-existent operation without changing the transaction', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); const unknownOperationId = Id.create(); @@ -696,7 +763,11 @@ describe('Transaction Domain Entity', () => { }); it('should reject deleting a non-existent operation without changing the transaction', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); const unknownOperationId = Id.create(); @@ -723,7 +794,11 @@ describe('Transaction Domain Entity', () => { }); it('Should add a new operation and increase version', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); @@ -809,7 +884,11 @@ describe('Transaction Domain Entity', () => { }); it('should update existing operations and increase the aggregate version once', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); @@ -898,7 +977,11 @@ describe('Transaction Domain Entity', () => { }); it('Should delete an existing operation, keep it in snapshot and hide it from active operations', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); @@ -961,7 +1044,11 @@ describe('Transaction Domain Entity', () => { describe('Deletion', () => { it('should mark transaction as deleted and all related operations, increase version and update timestamps', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); @@ -987,7 +1074,11 @@ describe('Transaction Domain Entity', () => { }); it('should reject updates after transaction is deleted', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); transaction.markAsDeleted(); const deletedSnapshot = transaction.toSnapshot(); @@ -1015,7 +1106,11 @@ describe('Transaction Domain Entity', () => { }); it('should not allow deleting an already deleted transaction', () => { - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); transaction.markAsDeleted(); const originalSnapshot = transaction.toSnapshot(); @@ -1037,6 +1132,7 @@ describe('Transaction Domain Entity', () => { it('should accept a multi-currency transaction balanced by value even when amounts do not sum to zero', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'RUB'], + commodity, operations: [ { accountKey: 'USD', @@ -1056,13 +1152,14 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); expect(() => - Transaction.create(user.getId(), transactionData), + Transaction.create(user.getId(), commodity, transactionData), ).not.toThrow(); }); it('should reject a multi-currency transaction with balanced amounts but unbalanced values', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'RUB'], + commodity, operations: [ { accountKey: 'USD', @@ -1081,14 +1178,15 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - expect(() => Transaction.create(user.getId(), transactionData)).toThrow( - UnbalancedTransactionError, - ); + expect(() => + Transaction.create(user.getId(), commodity, transactionData), + ).toThrow(UnbalancedTransactionError); }); it('should validate a multi-currency operation patch by value rather than amount', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'RUB'], + commodity, operations: [ { accountKey: 'USD', @@ -1106,7 +1204,11 @@ describe('Transaction Domain Entity', () => { }); const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const operationToUpdate = transaction.getOperations()[1]; const error = captureThrownError(() => @@ -1146,6 +1248,7 @@ describe('Transaction Domain Entity', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: operationsData, settings: transactionRawData, user, @@ -1153,9 +1256,9 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - expect(() => Transaction.create(user.getId(), transactionData)).toThrow( - UnbalancedTransactionError, - ); + expect(() => + Transaction.create(user.getId(), commodity, transactionData), + ).toThrow(UnbalancedTransactionError); }); it('should reject update when resulting operations do not sum to zero', () => { @@ -1184,6 +1287,7 @@ describe('Transaction Domain Entity', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: operationsData, settings: transactionRawData, user, @@ -1193,7 +1297,11 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); const originalVersion = transaction.getVersion(); @@ -1225,6 +1333,7 @@ describe('Transaction Domain Entity', () => { it('should reject creation with zero operations', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: [], settings: transactionRawData, user, @@ -1233,7 +1342,7 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); const error = captureThrownError(() => - Transaction.create(user.getId(), transactionData), + Transaction.create(user.getId(), commodity, transactionData), ); expect(error).toBeInstanceOf(InsufficientOperationsError); @@ -1257,6 +1366,7 @@ describe('Transaction Domain Entity', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: operationsData, settings: transactionRawData, user, @@ -1264,13 +1374,13 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - expect(() => Transaction.create(user.getId(), transactionData)).toThrow( - InsufficientOperationsError, - ); + expect(() => + Transaction.create(user.getId(), commodity, transactionData), + ).toThrow(InsufficientOperationsError); }); it('should allow creation with the maximum number of operations', () => { - const transaction = Transaction.create(user.getId(), { + const transaction = Transaction.create(user.getId(), commodity, { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS), }); @@ -1282,7 +1392,7 @@ describe('Transaction Domain Entity', () => { it('should reject creation with more than the maximum number of operations', () => { const error = captureThrownError(() => - Transaction.create(user.getId(), { + Transaction.create(user.getId(), commodity, { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS + 1), }), @@ -1314,6 +1424,7 @@ describe('Transaction Domain Entity', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: operationsData, settings: transactionRawData, user, @@ -1321,7 +1432,11 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const originalSnapshot = transaction.toSnapshot(); const originalVersion = transaction.getVersion(); @@ -1344,7 +1459,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject an update that exceeds the maximum number of operations', () => { - const transaction = Transaction.create(user.getId(), { + const transaction = Transaction.create(user.getId(), commodity, { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS), }); @@ -1365,7 +1480,7 @@ describe('Transaction Domain Entity', () => { }); it('should allow replacing operations without exceeding the maximum', () => { - const transaction = Transaction.create(user.getId(), { + const transaction = Transaction.create(user.getId(), commodity, { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS), }); @@ -1404,6 +1519,7 @@ describe('Transaction Domain Entity', () => { const data = TransactionBuilder.request({ accounts: ['USD', 'EUR', 'RUB', 'TRY'], + commodity, operations: operationsData, settings: transactionRawData, user, @@ -1411,7 +1527,11 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create(user.getId(), transactionData); + const transaction = Transaction.create( + user.getId(), + commodity, + transactionData, + ); const initialVersion = transaction.getVersion(); diff --git a/apps/backend/src/domain/transactions/transaction.entity.ts b/apps/backend/src/domain/transactions/transaction.entity.ts index cf54a946..ee1fa00b 100644 --- a/apps/backend/src/domain/transactions/transaction.entity.ts +++ b/apps/backend/src/domain/transactions/transaction.entity.ts @@ -11,6 +11,7 @@ import { UnbalancedTransactionError, } from 'src/domain/domain.errors'; +import { Commodity } from '../commodities'; import { EntityIdentity, EntityTimestamps, @@ -67,16 +68,27 @@ export class Transaction { private postingDate: DateValue, private transactionDate: DateValue, public currency: Currency, + private readonly commodityId: ParentChildRelation, + public description: string, private version: Version, ) {} - static create(userId: Id, dto: CreateTransactionProps): Transaction { + static create( + userId: Id, + commodity: Commodity, + dto: CreateTransactionProps, + ): Transaction { const identity = EntityIdentity.create(); const timestamps = EntityTimestamps.create(); const softDelete = SoftDelete.create(); const ownership = ParentChildRelation.create(userId, identity.getId()); + const commodityRelation = ParentChildRelation.create( + commodity.getId(), + identity.getId(), + ); + const transaction = new Transaction( identity, timestamps, @@ -85,6 +97,7 @@ export class Transaction { dto.postingDate, dto.transactionDate, dto.currency, + commodityRelation, dto.description, Version.create(0), ); @@ -147,6 +160,7 @@ export class Transaction { static restore(data: TransactionSnapshotWithDetails): Transaction { const { + commodityId, createdAt, currency, description, @@ -172,6 +186,11 @@ export class Transaction { identity.getId(), ); + const commodityRelation = ParentChildRelation.create( + Id.restore(commodityId), + identity.getId(), + ); + const transaction = new Transaction( identity, timestamps, @@ -180,6 +199,7 @@ export class Transaction { DateValue.restore(postingDate), DateValue.restore(transactionDate), Currency.restore(currency), + commodityRelation, description, Version.restore(version), ); @@ -236,6 +256,7 @@ export class Transaction { private buildSnapshot(operations: Operation[]): TransactionSnapshot { return { + commodityId: this.commodityId.getParentId().valueOf(), createdAt: this.getCreatedAt().valueOf(), currency: this.currency.valueOf(), description: this.description, diff --git a/apps/backend/src/domain/transactions/types.ts b/apps/backend/src/domain/transactions/types.ts index 93de31f6..a6966623 100644 --- a/apps/backend/src/domain/transactions/types.ts +++ b/apps/backend/src/domain/transactions/types.ts @@ -31,6 +31,7 @@ export type CreateTransactionProps = { transactionDate: DateValue; operations: OperationProps[]; currency: Currency; + commodityId: UUID; }; export type OperationsPatch = { @@ -53,6 +54,7 @@ export type TransactionSnapshot = { transactionDate: IsoDateString; updatedAt: IsoDatetimeString; userId: UUID; + commodityId: UUID; operations: OperationSnapshot[]; version: number; currency: CurrencyCode; diff --git a/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts index bc36b4f5..2029f64e 100644 --- a/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts +++ b/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts @@ -1,4 +1,4 @@ -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { User } from 'src/domain'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -13,8 +13,11 @@ describe('OperationPersistenceMapper', () => { }); it('maps an operation snapshot to a persistence row', () => { + const commodity = createCommodity(user, { code: 'USD' }); + const { operations } = TransactionBuilder.transaction({ accounts: ['USD'], + commodity, operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'USD', amount: '-100', description: 'Credit' }, diff --git a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts index 83180613..b411816b 100644 --- a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts +++ b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts @@ -22,7 +22,7 @@ import { OperationRepository } from './operation.repository'; describe('OperationRepository', () => { let testDB: TestDB; - let user: UserDbRow; + let userSnapshot: UserDbRow; let transaction: Transaction; @@ -42,7 +42,11 @@ describe('OperationRepository', () => { beforeEach(async () => { testDB = new TestDB(); await testDB.setupTestDb(); - user = await testDB.createUser(); + userSnapshot = await testDB.createUser(); + + const commodity = CommodityPersistenceMapper.toDomain( + await testDB.createCommodity(userSnapshot.id), + ); const operationsData = [ { accountKey: 'USD', amount: '10000', description: '1' }, @@ -55,8 +59,9 @@ describe('OperationRepository', () => { data = TransactionBuilder.transaction({ accounts: ['USD', 'EUR'], + commodity, operations: operationsData, - user: UserPersistenceMapper.toDomain(user), + user: UserPersistenceMapper.toDomain(userSnapshot), }); transaction = data.transaction; @@ -99,7 +104,7 @@ describe('OperationRepository', () => { OperationPersistenceMapper.toDBRow(operation), ); - await operationRepository.save(user.id, operations, new Map()); + await operationRepository.save(userSnapshot.id, operations, new Map()); const fetchedTransactionRelationsAfterSaving = await testDB.getTransactionWithRelations(transaction.getId().valueOf()); @@ -179,7 +184,7 @@ describe('OperationRepository', () => { ]; await operationRepository.save( - user.id, + userSnapshot.id, [...operationsToUpdateData, ...operationsToDeleteData], operationsSnapshot, ); @@ -307,7 +312,7 @@ describe('OperationRepository', () => { try { await operationRepository.save( - user.id, + userSnapshot.id, operationsToDeleteData, operationsSnapshot, ); @@ -366,7 +371,7 @@ describe('OperationRepository', () => { const operationToDelete = operations[0]; await operationRepository.save( - user.id, + userSnapshot.id, [ { ...operationToDelete, @@ -391,7 +396,7 @@ describe('OperationRepository', () => { await expect( operationRepository.save( - user.id, + userSnapshot.id, [ { ...operationToDelete, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts index 502e2fe8..8e8320bb 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts @@ -1,4 +1,4 @@ -import { createUser } from 'src/db/createTestUser'; +import { createCommodity, createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { describe, expect, it } from 'vitest'; @@ -7,8 +7,12 @@ import { TransactionPersistenceMapper } from './transaction-persistence.mapper'; describe('TransactionPersistenceMapper', () => { it('maps a transaction aggregate to a persistence row', async () => { const user = await createUser(); + + const commodity = createCommodity(user, { code: 'USD' }); + const { transaction } = TransactionBuilder.transaction({ accounts: ['USD'], + commodity, operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'USD', amount: '-100', description: 'Credit' }, @@ -18,6 +22,7 @@ describe('TransactionPersistenceMapper', () => { const snapshot = transaction.toSnapshot(); expect(TransactionPersistenceMapper.toDBRow(transaction)).toEqual({ + commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, currency: snapshot.currency, description: snapshot.description, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts index ec49b0f6..3e754bff 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts @@ -6,6 +6,7 @@ export class TransactionPersistenceMapper { const snapshot = transaction.toSnapshot(); return { + commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, currency: snapshot.currency, description: snapshot.description, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts index 2cb619b8..1362c261 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts @@ -16,6 +16,8 @@ describe('TransactionQueryRepository', () => { let testDB: TestDB; let transactionQueryRepo: TransactionQueryRepository; let user: UserDbRow; + let valuationCommodityId: UUID; + let usdAccount: TestAccount; let eurAccount: TestAccount; @@ -41,7 +43,7 @@ describe('TransactionQueryRepository', () => { }; const createTransaction = (seed: TransactionSeed) => - testDB.createTransactionFromSeed(user.id, seed); + testDB.createTransactionFromSeed(user.id, valuationCommodityId, seed); const expectTransactionToMatchSeed = ( transaction: Awaited>, @@ -81,11 +83,18 @@ describe('TransactionQueryRepository', () => { user = await testDB.createUser(); + const commodity = await testDB.createCommodity(user.id, { + code: CommodityCode.create('RUB').valueOf(), + }); + + valuationCommodityId = commodity.id; + const usdCommodityId = ( await testDB.createCommodity(user.id, { code: CommodityCode.create('USD').valueOf(), }) ).id; + const eurCommodityId = ( await testDB.createCommodity(user.id, { code: CommodityCode.create('EUR').valueOf(), diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts index d51a7b6f..971d5ffe 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts @@ -6,13 +6,10 @@ import { TransactionBuilder, TransactionPersistenceBuilderResult, } from 'src/db/test-utils'; -import { Account } from 'src/domain'; +import { Account, Commodity } from 'src/domain'; import { Amount, DateValue, Id, Version } from 'src/domain/domain-core'; import { OperationSnapshot } from 'src/domain/operations/types'; -import { - ForeignKeyConstraintError, - RepositoryNotFoundError, -} from 'src/infrastructure/errors'; +import { RepositoryNotFoundError } from 'src/infrastructure/errors'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -29,6 +26,7 @@ describe('TransactionRepository', () => { let testDB: TestDB; let transactionRepository: TransactionRepository; let user: UserDbRow; + let commodity: Commodity; let usdAccount: Account; let eurAccount: Account; let data: TransactionPersistenceBuilderResult; @@ -58,8 +56,13 @@ describe('TransactionRepository', () => { user = await testDB.createUser(); + commodity = CommodityPersistenceMapper.toDomain( + await testDB.createCommodity(user.id), + ); + data = TransactionBuilder.persistence({ accounts: ['USD', 'EUR'], + commodity, operations: [ { accountKey: 'USD', @@ -92,6 +95,7 @@ describe('TransactionRepository', () => { await testDB.insertCommodity( CommodityPersistenceMapper.toDBRowFromSnapshot(usdCommodity.toSnapshot()), ); + await testDB.insertCommodity( CommodityPersistenceMapper.toDBRowFromSnapshot(eurCommodity.toSnapshot()), ); @@ -105,14 +109,17 @@ describe('TransactionRepository', () => { await testDB.insertAccount( AccountPersistenceMapper.toDBRowFromSnapshot(usdAccount.toSnapshot()), ); + await testDB.insertAccount( AccountPersistenceMapper.toDBRowFromSnapshot(eurAccount.toSnapshot()), ); + await testDB.insertAccount( AccountPersistenceMapper.toDBRowFromSnapshot( usdSystemAccount.toSnapshot(), ), ); + await testDB.insertAccount( AccountPersistenceMapper.toDBRowFromSnapshot( eurSystemAccount.toSnapshot(), @@ -145,6 +152,7 @@ describe('TransactionRepository', () => { const insertedTransaction = await testDB.createTransactionWithOperations( user.id, + commodity.getId().valueOf(), { currencyCode: transaction.currency.valueOf(), description: transaction.description, @@ -159,9 +167,19 @@ describe('TransactionRepository', () => { insertedTransaction.id, ); + const retrievedTransactionSnapshot = retrievedTransaction?.toSnapshot(); + + expect(retrievedTransactionSnapshot?.commodityId).toEqual( + commodity.getId().valueOf(), + ); + expect(retrievedTransaction).not.toBeNull(); expect(retrievedTransaction?.description).toBe(transaction.description); + expect(retrievedTransaction?.getId().valueOf()).toBe( + insertedTransaction.id, + ); + expect(retrievedTransaction?.getPostingDate().valueOf()).toBe( transaction.getPostingDate().valueOf(), ); @@ -280,7 +298,7 @@ describe('TransactionRepository', () => { await expect( transactionRepository.create(nonExistentUserId, transaction), - ).rejects.toThrow(ForeignKeyConstraintError); + ).rejects.toThrow(RepositoryNotFoundError); }); it('should create a new transaction', async () => { diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts index 06335faa..16f2647a 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts @@ -5,7 +5,11 @@ import { TransactionRepositoryInterface, TransactionUpdateResult, } from 'src/application'; -import { TransactionWithRelations, transactionsTable } from 'src/db/schema'; +import { + TransactionWithRelations, + commoditiesTable, + transactionsTable, +} from 'src/db/schema'; import { Transaction } from 'src/domain'; import { Version } from 'src/domain/domain-core'; import { OperationSnapshot } from 'src/domain/operations/types'; @@ -59,6 +63,27 @@ export class TransactionRepository ): Promise { const transactionData = TransactionPersistenceMapper.toDBRow(transaction); + const existingCommodity = await this.db + .select() + .from(commoditiesTable) + .where( + and( + eq(commoditiesTable.id, transactionData.commodityId), + eq(commoditiesTable.userId, userId), + eq(commoditiesTable.isTombstone, false), + ), + ) + .get(); + + this.ensureEntityExists( + existingCommodity, + `Commodity with ID ${transaction.toSnapshot().commodityId} not found`, + this.entityNotFoundContext( + 'commodity', + transaction.toSnapshot().commodityId, + ), + ); + await this.db .insert(transactionsTable) .values({ ...transactionData, userId }); diff --git a/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts b/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts index ce7c247e..ebe13f56 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts @@ -92,6 +92,7 @@ describe('TransactionController', () => { describe('create', () => { it('should call CreateTransactionUseCase with correct parameters', async () => { const requestBody: TransactionCreateInput = { + commodityId: Id.create().valueOf(), currencyCode: Currency.create('USD').valueOf(), description: 'Test Transaction', operations, diff --git a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts index e75e8afc..a8a600c2 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts @@ -115,6 +115,7 @@ describe('Transactions Integration Tests', () => { it('should create a new transaction', async () => { const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'some transaction', operations: [operation1, operation2], @@ -158,6 +159,7 @@ describe('Transactions Integration Tests', () => { }); const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'USD to RUB exchange', operations: [ @@ -232,6 +234,7 @@ describe('Transactions Integration Tests', () => { it('should fail with invalid amounts', async () => { const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'invalid amount', operations: [ @@ -274,6 +277,7 @@ describe('Transactions Integration Tests', () => { ['missing value', { value: undefined as unknown as AmountString }], ])('should fail with %s', async (_, invalidOperationPatch) => { const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'invalid finite amount', operations: [ @@ -304,6 +308,7 @@ describe('Transactions Integration Tests', () => { it('should fail for unauthorized access', async () => { const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'unauthorized access', operations: [operation1, operation2], @@ -322,6 +327,7 @@ describe('Transactions Integration Tests', () => { it('should fail for non-existent accounts', async () => { const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'non-existent account', operations: [ @@ -346,6 +352,7 @@ describe('Transactions Integration Tests', () => { it('should fail when operations do not sum to zero (unbalanced transaction)', async () => { const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'unbalanced transaction', operations: [ @@ -388,6 +395,7 @@ describe('Transactions Integration Tests', () => { ); const payload: TransactionCreateInput = { + commodityId, currencyCode: Currency.create('USD').valueOf(), description: 'unauthorized access', operations: [ @@ -420,7 +428,7 @@ describe('Transactions Integration Tests', () => { beforeEach(async () => { const accounts = await createAccounts(); - transaction = await testDB.createTransaction(userId); + transaction = await testDB.createTransaction(userId, commodityId); const transaction1Operations = [ { @@ -524,7 +532,10 @@ describe('Transactions Integration Tests', () => { }); it('should not return soft-deleted (tombstone) operations', async () => { - const createdTransaction = await testDB.createTransaction(userId); + const createdTransaction = await testDB.createTransaction( + userId, + commodityId, + ); const deletedOperationsData = [ { @@ -618,7 +629,10 @@ describe('Transactions Integration Tests', () => { it("should return 404 when accessing another user's transaction", async () => { const otherUser = await testDB.createUser(); - const otherUserTransaction = await testDB.createTransaction(otherUser.id); + const otherUserTransaction = await testDB.createTransaction( + otherUser.id, + commodityId, + ); const response = await server.inject({ headers: { @@ -669,7 +683,7 @@ describe('Transactions Integration Tests', () => { await Promise.all( transactionData.map((tr) => { - return testDB.createTransaction(userId, tr); + return testDB.createTransaction(userId, commodityId, tr); }), ); @@ -728,9 +742,15 @@ describe('Transactions Integration Tests', () => { }); it('should not return soft-deleted (tombstone) transactions', async () => { - const transactionToBeDeleted = await testDB.createTransaction(userId); + const transactionToBeDeleted = await testDB.createTransaction( + userId, + commodityId, + ); - const otherTransaction = await testDB.createTransaction(userId); + const otherTransaction = await testDB.createTransaction( + userId, + commodityId, + ); await testDB.softDeleteTransaction(transactionToBeDeleted.id); @@ -761,7 +781,10 @@ describe('Transactions Integration Tests', () => { }); it('should not return soft-deleted (tombstone) operations in transactions', async () => { - const createdTransaction = await testDB.createTransaction(userId); + const createdTransaction = await testDB.createTransaction( + userId, + commodityId, + ); const deletedOperationsData = [ { @@ -860,8 +883,14 @@ describe('Transactions Integration Tests', () => { }); it('should return transactions filtered by accountId with all active operations', async () => { - const matchingTransaction = await testDB.createTransaction(userId); - const otherTransaction = await testDB.createTransaction(userId); + const matchingTransaction = await testDB.createTransaction( + userId, + commodityId, + ); + const otherTransaction = await testDB.createTransaction( + userId, + commodityId, + ); const matchingOperations = [ { @@ -932,7 +961,7 @@ describe('Transactions Integration Tests', () => { await Promise.all( transactionDates.map(([description, transactionDate]) => - testDB.createTransaction(userId, { + testDB.createTransaction(userId, commodityId, { description, transactionDate: DateValue.restore(transactionDate).valueOf(), }), @@ -966,7 +995,7 @@ describe('Transactions Integration Tests', () => { await Promise.all( transactionDates.map(([description, transactionDate]) => - testDB.createTransaction(userId, { + testDB.createTransaction(userId, commodityId, { description, transactionDate: DateValue.restore(transactionDate).valueOf(), }), @@ -1017,7 +1046,7 @@ describe('Transactions Integration Tests', () => { describe('DELETE /api/transactions/:id', () => { it('should delete an existing transaction and return 204', async () => { const [account1, account2] = await createAccounts(); - const transaction = await testDB.createTransaction(userId); + const transaction = await testDB.createTransaction(userId, commodityId); const operations = await Promise.all([ testDB.createOperation(userId, { accountId: account1.id, @@ -1081,7 +1110,7 @@ describe('Transactions Integration Tests', () => { }); it('should return 404 when deleting an already deleted transaction', async () => { - const transaction = await testDB.createTransaction(userId); + const transaction = await testDB.createTransaction(userId, commodityId); await testDB.softDeleteTransaction(transaction.id); @@ -1097,7 +1126,7 @@ describe('Transactions Integration Tests', () => { }); it('should return 401 when deleting without authorization', async () => { - const transaction = await testDB.createTransaction(userId); + const transaction = await testDB.createTransaction(userId, commodityId); const response = await server.inject({ method: 'DELETE', @@ -1108,7 +1137,7 @@ describe('Transactions Integration Tests', () => { }); it('should not return deleted transaction in GET /api/transactions', async () => { - const transaction = await testDB.createTransaction(userId); + const transaction = await testDB.createTransaction(userId, commodityId); await testDB.softDeleteTransaction(transaction.id); @@ -1138,7 +1167,10 @@ describe('Transactions Integration Tests', () => { password: 'password123', }); - const otherUserTransaction = await testDB.createTransaction(otherUser.id); + const otherUserTransaction = await testDB.createTransaction( + otherUser.id, + commodityId, + ); const response = await server.inject({ headers: { @@ -1205,7 +1237,10 @@ describe('Transactions Integration Tests', () => { }); it('should update metadata without changing operations', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); await testDB.createOperation(userId, { ...operation1Data, @@ -1258,7 +1293,10 @@ describe('Transactions Integration Tests', () => { }); it('should create operations without changing existing operations', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const createdOperations = await Promise.all([ testDB.createOperation(userId, { @@ -1335,7 +1373,10 @@ describe('Transactions Integration Tests', () => { }); it('should update existing operations and persist their changes', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const createdOperations = await Promise.all([ testDB.createOperation(userId, { @@ -1409,7 +1450,10 @@ describe('Transactions Integration Tests', () => { }); it('should apply create, update, and delete operations in one request', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const createdOperations = await Promise.all([ testDB.createOperation(userId, { @@ -1518,7 +1562,10 @@ describe('Transactions Integration Tests', () => { }); it('should delete operations from the active response', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const createdOperations = await Promise.all([ testDB.createOperation(userId, { @@ -1651,8 +1698,11 @@ describe('Transactions Integration Tests', () => { ])( 'should reject $name without persisting partial changes', async ({ buildOperations }) => { - const transaction = - await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); + const createdOperations = await Promise.all([ testDB.createOperation(userId, { ...operation1Data, @@ -1717,7 +1767,7 @@ describe('Transactions Integration Tests', () => { }); it('should return 404 when updating a soft-deleted transaction', async () => { - const transaction = await testDB.createTransaction(userId); + const transaction = await testDB.createTransaction(userId, commodityId); await testDB.softDeleteTransaction(transaction.id); @@ -1730,7 +1780,10 @@ describe('Transactions Integration Tests', () => { it("should return 404 when updating another user's transaction", async () => { const another = await testDB.createUser(); - const transaction = await testDB.createTransaction(another.id); + const transaction = await testDB.createTransaction( + another.id, + commodityId, + ); const updatedData = createUpdateRequest(); @@ -1740,18 +1793,22 @@ describe('Transactions Integration Tests', () => { }); it("should return 404 when creating operations with another user's account", async () => { - const transaction = await testDB.createTransactionWithOperations(userId, { - operations: [ - { - ...operation1Data, - id: Id.create().valueOf(), - }, - { - ...operation2Data, - id: Id.create().valueOf(), - }, - ], - }); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + { + operations: [ + { + ...operation1Data, + id: Id.create().valueOf(), + }, + { + ...operation2Data, + id: Id.create().valueOf(), + }, + ], + }, + ); const createdOperations = transaction.operations; const otherUser = await testDB.createUser(); @@ -1823,18 +1880,22 @@ describe('Transactions Integration Tests', () => { }); it("should return 404 when updating operations to another user's account", async () => { - const transaction = await testDB.createTransactionWithOperations(userId, { - operations: [ - { - ...operation1Data, - id: Id.create().valueOf(), - }, - { - ...operation2Data, - id: Id.create().valueOf(), - }, - ], - }); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + { + operations: [ + { + ...operation1Data, + id: Id.create().valueOf(), + }, + { + ...operation2Data, + id: Id.create().valueOf(), + }, + ], + }, + ); const createdOperations = transaction.operations; const otherUser = await testDB.createUser(); @@ -1901,7 +1962,10 @@ describe('Transactions Integration Tests', () => { }); it('should return 400 for invalid payload (missing required fields)', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const invalidPayload = { description: 'Updated description', @@ -1926,7 +1990,10 @@ describe('Transactions Integration Tests', () => { }); it('should return 401 when not authorized', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const updatedData = createUpdateRequest(); @@ -1940,23 +2007,27 @@ describe('Transactions Integration Tests', () => { }); it('should return 409 on optimistic locking conflict with a stale version', async () => { - const transaction = await testDB.createTransactionFromSeed(userId, { - description: 'Transaction for optimistic locking', - operations: [ - { - account: account1Data, - amount: '-100', - description: 'Transfer from checking', - value: '-100', - }, - { - account: account2Data, - amount: '100', - description: 'Transfer to savings', - value: '100', - }, - ], - }); + const transaction = await testDB.createTransactionFromSeed( + userId, + commodityId, + { + description: 'Transaction for optimistic locking', + operations: [ + { + account: account1Data, + amount: '-100', + description: 'Transfer from checking', + value: '-100', + }, + { + account: account2Data, + amount: '100', + description: 'Transfer to savings', + value: '100', + }, + ], + }, + ); const firstUpdate = createUpdateRequest({ description: 'First successful update', @@ -2011,7 +2082,10 @@ describe('Transactions Integration Tests', () => { }); it('should return 400 when resulting operations are unbalanced (sum != 0)', async () => { - const transaction = await testDB.createTransactionWithOperations(userId); + const transaction = await testDB.createTransactionWithOperations( + userId, + commodityId, + ); const operationToUpdate = await testDB.createOperation(userId, { ...operation1Data, diff --git a/packages/shared/src/types/transaction.ts b/packages/shared/src/types/transaction.ts index c0bf2ee4..c0cbc707 100644 --- a/packages/shared/src/types/transaction.ts +++ b/packages/shared/src/types/transaction.ts @@ -43,6 +43,7 @@ export type OperationResponseDTO = { }; export type CreateTransactionRequestDTO = { + commodityId: UUID; currencyCode: CurrencyCode; description: string; operations: CreateOperationRequestDTO[]; diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index 18090a27..ccf62b23 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -35,6 +35,7 @@ export const operationUpdateSchema = z.object({ }); export const transactionCreateSchema = z.object({ + commodityId: uuid, currencyCode: currencyCode, description: requiredText, operations: z From fe67fe45e3e98c3b7587dfd3b38d8c85b9abf343 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 09:13:04 +0300 Subject: [PATCH 09/30] LED-66 refactor(accounts): remove currency references from account handling --- .../backend/drizzle/0001_overrated_marrow.sql | 15 + apps/backend/drizzle/meta/0001_snapshot.json | 614 ++++++++++++++++++ apps/backend/drizzle/meta/_journal.json | 7 + .../src/application/dto/currency.dto.ts | 13 - apps/backend/src/application/dto/index.ts | 1 - .../interfaces/AccountRepository.interface.ts | 11 +- .../mappers/account.mapper.test.ts | 6 +- .../src/application/mappers/account.mapper.ts | 2 - .../mappers/operation.mapper.test.ts | 7 +- .../transaction-read-model-response.mapper.ts | 1 - .../mappers/transaction.mapper.test.ts | 8 +- .../application/mappers/transaction.mapper.ts | 3 - .../read-models/transaction.read-model.ts | 2 - .../transaction.context-loader.test.ts | 11 +- .../transaction.context-loader.ts | 35 +- .../accounts/__tests__/createAccount.test.ts | 6 - .../accounts/__tests__/deleteAccount.test.ts | 3 - .../accounts/__tests__/getAccountById.test.ts | 8 +- .../accounts/__tests__/getAllAccounts.test.ts | 4 +- .../accounts/__tests__/updateAccount.test.ts | 8 +- .../usecases/accounts/createAccount.ts | 6 +- .../__tests__/createTransaction.test.ts | 98 +-- .../__tests__/deleteTransaction.test.ts | 6 +- .../__tests__/getAllTransactions.test.ts | 9 +- .../__tests__/getTransactionById.test.ts | 10 +- .../__tests__/updateTransaction.test.ts | 6 +- apps/backend/src/db/createTestUser.ts | 4 - apps/backend/src/db/schema.ts | 1 - apps/backend/src/db/schemas/accounts.ts | 10 +- apps/backend/src/db/schemas/currencies.ts | 8 - apps/backend/src/db/schemas/index.ts | 1 - apps/backend/src/db/schemas/settings.ts | 5 - apps/backend/src/db/schemas/transactions.ts | 3 +- apps/backend/src/db/scripts/currenciesSeed.ts | 56 -- apps/backend/src/db/test-db.ts | 55 +- .../backend/src/db/test-utils/builder.test.ts | 30 +- .../src/db/test-utils/prettyPrint.test.ts | 14 +- apps/backend/src/db/test-utils/prettyPrint.ts | 43 +- .../src/db/test-utils/testEntityBuilder.ts | 143 ++-- apps/backend/src/di/container.ts | 3 - apps/backend/src/di/types.ts | 2 - .../src/domain/accounts/account.entity.ts | 16 - .../src/domain/accounts/account.test.ts | 23 +- apps/backend/src/domain/accounts/types.ts | 3 - .../domain-core/value-objects/Currency.ts | 49 -- .../domain/domain-core/value-objects/index.ts | 1 - .../value-object-api-conventions.test.ts | 6 - .../domain/domain-entity-identity-api.test.ts | 2 - .../operations/operation.entity.test.ts | 8 +- .../transactions/transaction.entity.test.ts | 42 +- .../domain/transactions/transaction.entity.ts | 6 - apps/backend/src/domain/transactions/types.ts | 12 +- .../account-persistence.mapper.test.ts | 5 +- .../db/accounts/account-persistence.mapper.ts | 2 - .../db/accounts/account.repository.test.ts | 70 +- .../db/accounts/account.repository.ts | 31 +- .../db/currency/currency.repository.ts | 13 - .../src/infrastructure/db/currency/index.ts | 1 - apps/backend/src/infrastructure/db/index.ts | 1 - .../operation-persistence.mapper.test.ts | 7 +- .../operations/operation.repository.test.ts | 7 +- .../transaction-persistence.mapper.test.ts | 8 +- .../transaction-persistence.mapper.ts | 1 - .../transaction-query.repository.test.ts | 9 +- .../transaction-read-model.mapper.ts | 1 - .../transaction.repository.test.ts | 32 +- .../db/transaction/transaction.repository.ts | 1 - .../formatters/AmountFormatter.ts | 38 -- .../http/accounts/account.controller.test.ts | 3 - .../http/accounts/account.integration.test.ts | 7 - .../transaction.controller.test.ts | 6 +- .../transaction.integration.test.ts | 20 +- packages/shared/src/types/account.ts | 9 +- packages/shared/src/types/transaction.ts | 3 - packages/shared/src/types/types.ts | 3 - packages/shared/src/validation/accounts.ts | 4 - .../shared/src/validation/baseValidations.ts | 5 - .../shared/src/validation/transactions.ts | 2 - 78 files changed, 870 insertions(+), 865 deletions(-) create mode 100644 apps/backend/drizzle/0001_overrated_marrow.sql create mode 100644 apps/backend/drizzle/meta/0001_snapshot.json delete mode 100644 apps/backend/src/application/dto/currency.dto.ts delete mode 100644 apps/backend/src/db/schemas/currencies.ts delete mode 100644 apps/backend/src/db/scripts/currenciesSeed.ts delete mode 100644 apps/backend/src/domain/domain-core/value-objects/Currency.ts delete mode 100644 apps/backend/src/infrastructure/db/currency/currency.repository.ts delete mode 100644 apps/backend/src/infrastructure/db/currency/index.ts diff --git a/apps/backend/drizzle/0001_overrated_marrow.sql b/apps/backend/drizzle/0001_overrated_marrow.sql new file mode 100644 index 00000000..7bf70945 --- /dev/null +++ b/apps/backend/drizzle/0001_overrated_marrow.sql @@ -0,0 +1,15 @@ +DROP TABLE `currencies`;--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_settings` ( + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + `user_id` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +INSERT INTO `__new_settings`("created_at", "updated_at", "user_id") SELECT "created_at", "updated_at", "user_id" FROM `settings`;--> statement-breakpoint +DROP TABLE `settings`;--> statement-breakpoint +ALTER TABLE `__new_settings` RENAME TO `settings`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +ALTER TABLE `accounts` DROP COLUMN `currency`;--> statement-breakpoint +ALTER TABLE `transactions` DROP COLUMN `currency`; \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..c8fc7f1c --- /dev/null +++ b/apps/backend/drizzle/meta/0001_snapshot.json @@ -0,0 +1,614 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f9ae6d92-c304-4529-a9ee-11183f004cff", + "prevId": "67f3394c-dc2e-4f8d-95c2-f74c1aef120c", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "commodity_id": { + "name": "commodity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "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", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initial_balance": { + "name": "initial_balance", + "type": "text", + "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", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_name_unique_idx": { + "name": "user_id_name_unique_idx", + "columns": [ + "user_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "accounts_commodity_id_commodities_id_fk": { + "name": "accounts_commodity_id_commodities_id_fk", + "tableFrom": "accounts", + "tableTo": "commodities", + "columnsFrom": [ + "commodity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transactions": { + "name": "transactions", + "columns": { + "valuation_commodity_id": { + "name": "valuation_commodity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "posting_date": { + "name": "posting_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transaction_date": { + "name": "transaction_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_transactions_user_date": { + "name": "idx_transactions_user_date", + "columns": [ + "user_id", + "transaction_date" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transactions_valuation_commodity_id_commodities_id_fk": { + "name": "transactions_valuation_commodity_id_commodities_id_fk", + "tableFrom": "transactions", + "tableTo": "commodities", + "columnsFrom": [ + "valuation_commodity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "operations": { + "name": "operations", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_operations_transaction": { + "name": "idx_operations_transaction", + "columns": [ + "transaction_id" + ], + "isUnique": false + }, + "idx_operations_account": { + "name": "idx_operations_account", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "idx_operations_user": { + "name": "idx_operations_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "operations_account_id_accounts_id_fk": { + "name": "operations_account_id_accounts_id_fk", + "tableFrom": "operations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "operations_transaction_id_transactions_id_fk": { + "name": "operations_transaction_id_transactions_id_fk", + "tableFrom": "operations", + "tableTo": "transactions", + "columnsFrom": [ + "transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "operations_user_id_users_id_fk": { + "name": "operations_user_id_users_id_fk", + "tableFrom": "operations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_users_id_fk": { + "name": "settings_user_id_users_id_fk", + "tableFrom": "settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "commodities": { + "name": "commodities", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_tombstone": { + "name": "is_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "precision": { + "name": "precision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "commodities_user_id_code_unique_idx": { + "name": "commodities_user_id_code_unique_idx", + "columns": [ + "user_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "commodities_user_id_users_id_fk": { + "name": "commodities_user_id_users_id_fk", + "tableFrom": "commodities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index d223fcf6..23360456 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1784739467705, "tag": "0000_nosy_professor_monster", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1784743725510, + "tag": "0001_overrated_marrow", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/backend/src/application/dto/currency.dto.ts b/apps/backend/src/application/dto/currency.dto.ts deleted file mode 100644 index aa9d3600..00000000 --- a/apps/backend/src/application/dto/currency.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; - -// Response DTOs -export type CurrencyResponseDTO = { - code: CurrencyCode; - name: string; - symbol: string; -}; - -// Query DTOs -export type GetCurrenciesQueryDTO = { - codes?: CurrencyCode[]; -}; diff --git a/apps/backend/src/application/dto/index.ts b/apps/backend/src/application/dto/index.ts index aa46f60f..4e643cb6 100644 --- a/apps/backend/src/application/dto/index.ts +++ b/apps/backend/src/application/dto/index.ts @@ -1,4 +1,3 @@ export * from './transaction.dto'; export * from './operation.dto'; -export * from './currency.dto'; export * from './user.dto'; diff --git a/apps/backend/src/application/interfaces/AccountRepository.interface.ts b/apps/backend/src/application/interfaces/AccountRepository.interface.ts index 001c0213..a9abfdbf 100644 --- a/apps/backend/src/application/interfaces/AccountRepository.interface.ts +++ b/apps/backend/src/application/interfaces/AccountRepository.interface.ts @@ -1,12 +1,9 @@ -import { CurrencyCode, UUID } from '@ledgerly/shared/types'; +import { UUID } from '@ledgerly/shared/types'; import { AccountSnapshot } from 'src/domain/accounts'; export type AccountRepositoryUpdateInput = Pick & Partial< - Pick< - AccountSnapshot, - 'currency' | 'description' | 'initialBalance' | 'name' | 'type' - > + Pick >; export type AccountRepositorySoftDeleteInput = Pick< @@ -28,10 +25,6 @@ export type AccountRepositoryInterface = { id: UUID, data: AccountRepositorySoftDeleteInput, ): Promise; - findSystemAccount( - userId: UUID, - currency: CurrencyCode, - ): Promise; ensureUserOwnsAccount( userId: UUID, accountId: UUID, diff --git a/apps/backend/src/application/mappers/account.mapper.test.ts b/apps/backend/src/application/mappers/account.mapper.test.ts index b572a570..62745e4f 100644 --- a/apps/backend/src/application/mappers/account.mapper.test.ts +++ b/apps/backend/src/application/mappers/account.mapper.test.ts @@ -1,6 +1,6 @@ import { AccountUpdateDTO } from '@ledgerly/shared/types'; import { AccountSnapshot } from 'src/domain/accounts'; -import { Amount, Currency, Id, Timestamp } from 'src/domain/domain-core'; +import { Amount, Id, Timestamp } from 'src/domain/domain-core'; import { describe, expect, it } from 'vitest'; import { AccountMapper } from './account.mapper'; @@ -9,7 +9,6 @@ describe('AccountMapper', () => { const snapshot: AccountSnapshot = { commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency: Currency.create('USD').valueOf(), currentClearedBalanceLocal: Amount.create('2500').valueOf(), description: 'Operating account', id: Id.create().valueOf(), @@ -26,7 +25,6 @@ describe('AccountMapper', () => { expect(AccountMapper.toResponseDTOFromSnapshot(snapshot)).toEqual({ commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, - currency: snapshot.currency, currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal, description: snapshot.description, id: snapshot.id, @@ -42,7 +40,6 @@ describe('AccountMapper', () => { it('maps a full update DTO to domain update props', () => { const dto: AccountUpdateDTO = { - currency: Currency.create('EUR').valueOf(), description: 'Updated description', isSystem: false, name: 'Updated Account', @@ -50,7 +47,6 @@ describe('AccountMapper', () => { }; expect(AccountMapper.toUpdateProps(dto)).toEqual({ - currency: dto.currency, description: dto.description, name: dto.name, type: dto.type, diff --git a/apps/backend/src/application/mappers/account.mapper.ts b/apps/backend/src/application/mappers/account.mapper.ts index 7e5aa206..eae91e68 100644 --- a/apps/backend/src/application/mappers/account.mapper.ts +++ b/apps/backend/src/application/mappers/account.mapper.ts @@ -8,7 +8,6 @@ export class AccountMapper { return { commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, - currency: snapshot.currency, currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal, description: snapshot.description, id: snapshot.id, @@ -24,7 +23,6 @@ export class AccountMapper { static toUpdateProps(dto: AccountUpdateDTO): AccountUpdateProps { return { - currency: dto.currency, description: dto.description, name: dto.name, type: dto.type, diff --git a/apps/backend/src/application/mappers/operation.mapper.test.ts b/apps/backend/src/application/mappers/operation.mapper.test.ts index 6ac3c9ed..0f32663d 100644 --- a/apps/backend/src/application/mappers/operation.mapper.test.ts +++ b/apps/backend/src/application/mappers/operation.mapper.test.ts @@ -3,7 +3,7 @@ import { CreateOperationRequestDTO, UpdateOperationRequestDTO, } from 'src/application/dto'; -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { User } from 'src/domain'; import { @@ -25,11 +25,8 @@ describe('OperationMapper', () => { beforeAll(async () => { user = await createUser(); - const commodity = createCommodity(user, { code: 'USD' }); - const fixture = TransactionBuilder.request({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'USD', amount: '-100', description: 'Credit' }, diff --git a/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts b/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts index b4b20302..c974ad1a 100644 --- a/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts +++ b/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts @@ -7,7 +7,6 @@ export class TransactionReadModelResponseMapper { ): TransactionResponseDTO { return { createdAt: transaction.createdAt, - currency: transaction.currency, description: transaction.description, id: transaction.id, // mapOperation does not access this, so passing it unbound is safe. diff --git a/apps/backend/src/application/mappers/transaction.mapper.test.ts b/apps/backend/src/application/mappers/transaction.mapper.test.ts index bed5cbe9..7da5bd07 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.test.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.test.ts @@ -1,4 +1,4 @@ -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { Transaction } from 'src/domain'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,11 +10,9 @@ describe('TransactionMapper', () => { beforeAll(async () => { const user = await createUser(); - const commodity = createCommodity(user, { code: 'USD' }); fixture = TransactionBuilder.transaction({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [ { accountKey: 'USD', @@ -28,7 +26,6 @@ describe('TransactionMapper', () => { }, ], settings: { - currencyCode: 'USD', description: 'Transaction with tombstone operation', postingDate: '2026-07-10', transactionDate: '2026-07-09', @@ -67,7 +64,6 @@ describe('TransactionMapper', () => { expect(response).toEqual({ createdAt: transactionSnapshot.createdAt, - currency: transactionSnapshot.currency, description: transactionSnapshot.description, id: transactionSnapshot.id, operations: [ diff --git a/apps/backend/src/application/mappers/transaction.mapper.ts b/apps/backend/src/application/mappers/transaction.mapper.ts index 9cbf5a25..8e3fe52d 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.ts @@ -1,5 +1,4 @@ import { Transaction } from 'src/domain'; -import { Currency } from 'src/domain/domain-core/value-objects/Currency'; import { DateValue } from 'src/domain/domain-core/value-objects/DateValue'; import { CreateTransactionProps, @@ -17,7 +16,6 @@ export class TransactionMapper { return { createdAt: snapshot.createdAt, - currency: snapshot.currency, description: snapshot.description, id: snapshot.id, operations: snapshot.operations.map((operation) => @@ -41,7 +39,6 @@ export class TransactionMapper { return { commodityId: dto.commodityId, - currency: Currency.create(dto.currencyCode), description: dto.description, operations, postingDate: DateValue.restore(dto.postingDate), diff --git a/apps/backend/src/application/read-models/transaction.read-model.ts b/apps/backend/src/application/read-models/transaction.read-model.ts index 4bf09ef1..6e3d755e 100644 --- a/apps/backend/src/application/read-models/transaction.read-model.ts +++ b/apps/backend/src/application/read-models/transaction.read-model.ts @@ -1,5 +1,4 @@ import { - CurrencyCode, IsoDateString, IsoDatetimeString, AmountString, @@ -21,7 +20,6 @@ export type OperationReadModel = { export type TransactionReadModel = { createdAt: IsoDatetimeString; - currency: CurrencyCode; description: string; id: UUID; operations: OperationReadModel[]; diff --git a/apps/backend/src/application/services/TransactionService/__tests__/transaction.context-loader.test.ts b/apps/backend/src/application/services/TransactionService/__tests__/transaction.context-loader.test.ts index d7f6562f..af478ed6 100644 --- a/apps/backend/src/application/services/TransactionService/__tests__/transaction.context-loader.test.ts +++ b/apps/backend/src/application/services/TransactionService/__tests__/transaction.context-loader.test.ts @@ -1,9 +1,8 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; import { OperationRequestDTO } from 'src/application/dto'; import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { createAccount } from 'src/db/createTestUser'; import { User } from 'src/domain'; -import { Amount, Currency } from 'src/domain/domain-core'; +import { Amount } from 'src/domain/domain-core'; import { createUser } from 'src/testing'; import { beforeAll, describe, expect, it, vi, beforeEach } from 'vitest'; @@ -32,16 +31,10 @@ describe('TransactionContextLoader', () => { const account1 = createAccount(user); const account2 = createAccount(user); const account3 = createAccount(user); - const account4 = createAccount(user, { currency: Currency.create('EUR') }); + const account4 = createAccount(user); const accounts = [account1, account2, account3, account4]; - const currenciesSet = new Set(); - - for (const acc of accounts) { - currenciesSet.add(acc.currency.valueOf()); - } - const rawOperations: OperationRequestDTO[] = [ { accountId: account1.getId().valueOf(), diff --git a/apps/backend/src/application/services/TransactionService/transaction.context-loader.ts b/apps/backend/src/application/services/TransactionService/transaction.context-loader.ts index 1e2abd5a..8b40e50e 100644 --- a/apps/backend/src/application/services/TransactionService/transaction.context-loader.ts +++ b/apps/backend/src/application/services/TransactionService/transaction.context-loader.ts @@ -1,4 +1,4 @@ -import { CurrencyCode, UUID } from '@ledgerly/shared/types'; +import { UUID } from '@ledgerly/shared/types'; import { Account, User } from 'src/domain'; import { TransactionBuildContext } from 'src/domain/transactions/types'; @@ -14,10 +14,8 @@ export class TransactionContextLoader { operations: OperationRequestDTO[], ): Promise<{ accountsMap: Map; - currenciesSet: Set; }> { const accountIds = new Set(); - const currenciesSet = new Set(); for (const operation of operations) { accountIds.add(operation.accountId); @@ -31,41 +29,16 @@ export class TransactionContextLoader { const accountsMap = new Map(); for (const snapshot of accountRows) { - currenciesSet.add(snapshot.currency); accountsMap.set(snapshot.id, Account.restore(snapshot)); } - return { accountsMap, currenciesSet }; + return { accountsMap }; } - // TODO: return this method when system accounts are needed for some operation type (e.g. currency trading) and implement findOrCreateSystemAccount in AccountFactory - // private async preloadSystemAccounts( - // user: User, - // currenciesSet: Set, - // ): Promise> { - // const systemAccountsMap = new Map(); - - // for (const currency of currenciesSet) { - // const systemAccount = await this.accountFactory.findOrCreateSystemAccount( - // user, - // currency, - // ); - - // systemAccountsMap.set(currency, systemAccount); - // } - - // return systemAccountsMap; - // } - - async loadContext( + loadContext( user: User, operations: OperationRequestDTO[], ): Promise { - const { accountsMap } = await this.preloadAccounts(user, operations); - - // TODO: Return system account when it is needed for some operation type (e.g. currency trading) and implement findOrCreateSystemAccount in AccountFactory - const systemAccountsMap = new Map(); - - return { accountsMap, systemAccountsMap }; + return this.preloadAccounts(user, operations); } } diff --git a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts index cfd31bff..70e6cf20 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts @@ -1,4 +1,3 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; import { AccountRepositoryInterface, CommodityRepositoryInterface, @@ -27,7 +26,6 @@ describe('CreateAccountUseCase', async () => { const description = 'Test account description'; const initialBalance = Amount.create('1000').valueOf(); const currentClearedBalanceLocal = Amount.create('0').valueOf(); - const currency = 'USD' as CurrencyCode; const commodity = Commodity.create( user, @@ -55,7 +53,6 @@ describe('CreateAccountUseCase', async () => { // Act const result = await createAccountUseCase.execute(user, { commodityId: mockedCommoditySnapshot.id, - currency: currency, description, initialBalance, name, @@ -70,7 +67,6 @@ describe('CreateAccountUseCase', async () => { expect(accountRepository.create).toHaveBeenCalledWith({ commodityId: result.commodityId, createdAt: result.createdAt, - currency, currentClearedBalanceLocal, description, id: result.id, @@ -84,7 +80,6 @@ describe('CreateAccountUseCase', async () => { }); expect(result).toMatchObject({ - currency, description, initialBalance, isSystem: false, @@ -117,7 +112,6 @@ describe('CreateAccountUseCase', async () => { await expect( createAccountUseCase.execute(user, { commodityId: mockedAnotherCommoditySnapshot.id, - currency: currency, description, initialBalance, name, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts index 3e9b3cde..a6ffa5e7 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts @@ -1,4 +1,3 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; import { createUser } from 'src/db/createTestUser'; @@ -28,7 +27,6 @@ describe('DeleteAccountUseCase', async () => { const accountName = 'Test Account'; const description = 'Test account description'; const initialBalance = Amount.create('1000').valueOf(); - const currencyCode = 'USD' as CurrencyCode; const accountType = 'asset'; const mockUser = { @@ -41,7 +39,6 @@ describe('DeleteAccountUseCase', async () => { const mockAccountData: AccountSnapshot = { commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency: currencyCode, currentClearedBalanceLocal: initialBalance, description, id: Id.create().valueOf(), diff --git a/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts index ff05a204..624b701c 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/getAccountById.test.ts @@ -1,8 +1,4 @@ -import { - AccountTypeValue, - apiErrorCodes, - CurrencyCode, -} from '@ledgerly/shared/types'; +import { AccountTypeValue, apiErrorCodes } from '@ledgerly/shared/types'; import { EntityNotFoundError, UnauthorizedAccessError, @@ -35,7 +31,6 @@ describe('GetAccountByIdUseCase', async () => { const accountName = 'Test Account'; const description = 'Test account description'; const initialBalance = Amount.create('1000').valueOf(); - const currency = 'USD' as CurrencyCode; const accountType = 'asset' as AccountTypeValue; const mockUser = { @@ -48,7 +43,6 @@ describe('GetAccountByIdUseCase', async () => { const mockSavedAccountData: AccountSnapshot = { commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency, currentClearedBalanceLocal: initialBalance, description, id: accountId, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts index ec601078..3200089e 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/getAllAccounts.test.ts @@ -1,4 +1,4 @@ -import { AccountTypeValue, CurrencyCode } from '@ledgerly/shared/types'; +import { AccountTypeValue } from '@ledgerly/shared/types'; import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; import { createUser } from 'src/db/createTestUser'; @@ -24,13 +24,11 @@ describe('GetAllAccounts', async () => { '550e8400-e29b-41d4-a716-446655440001', ).valueOf(); const initialBalance = Amount.create('1000').valueOf(); - const currency = 'USD' as CurrencyCode; const accountType = 'asset' as AccountTypeValue; const mockSavedAccountData: AccountSnapshot = { commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency, currentClearedBalanceLocal: initialBalance, description, id: accountId, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts index 1604e63c..c7e4fb66 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts @@ -1,8 +1,4 @@ -import { - AccountTypeValue, - apiErrorCodes, - CurrencyCode, -} from '@ledgerly/shared/types'; +import { AccountTypeValue, apiErrorCodes } from '@ledgerly/shared/types'; import { EntityNotFoundError } from 'src/application/application.errors'; import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; @@ -33,13 +29,11 @@ describe('UpdateAccount', async () => { const accountName = 'Test Account'; const description = 'Test account description'; const initialBalance = Amount.create('1000').valueOf(); - const currency = 'USD' as CurrencyCode; const accountType = 'asset' as AccountTypeValue; const mockAccountData: AccountSnapshot = { commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency, currentClearedBalanceLocal: initialBalance, description, id: accountId, diff --git a/apps/backend/src/application/usecases/accounts/createAccount.ts b/apps/backend/src/application/usecases/accounts/createAccount.ts index 8f58a0c8..066dc2af 100644 --- a/apps/backend/src/application/usecases/accounts/createAccount.ts +++ b/apps/backend/src/application/usecases/accounts/createAccount.ts @@ -5,7 +5,7 @@ import type { } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; import { AccountType, Account, Commodity } from 'src/domain/'; -import { Amount, Currency, Name } from 'src/domain/domain-core'; +import { Amount, Name } from 'src/domain/domain-core'; import { User } from 'src/domain/users/user.entity'; export class CreateAccountUseCase { @@ -17,8 +17,7 @@ export class CreateAccountUseCase { user: User, data: AccountCreateDTO, ): Promise { - const { commodityId, currency, description, initialBalance, name, type } = - data; + const { commodityId, description, initialBalance, name, type } = data; const commoditySnapshot = await this.commodityRepository.getById( user.getId().valueOf(), @@ -33,7 +32,6 @@ export class CreateAccountUseCase { Name.create(name), description, Amount.create(initialBalance), - Currency.create(currency), AccountType.create(type), ); diff --git a/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts index 91367344..557ba0b9 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts @@ -6,9 +6,7 @@ import type { import { TransactionContextLoader } from 'src/application/services/TransactionService'; import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils/testEntityBuilder'; -import { User, Transaction, Commodity } from 'src/domain'; -import { CommodityCode } from 'src/domain/domain-core/value-objects/CommodityCode'; -import { Name } from 'src/domain/domain-core/value-objects/Name'; +import { User, Transaction } from 'src/domain'; import { UnbalancedTransactionError } from 'src/domain/domain.errors'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -16,7 +14,6 @@ import { CreateTransactionUseCase } from '../CreateTransaction'; describe('CreateTransactionUseCase', () => { let user: User; - let commodity: Commodity; const mockTransactionRepository = { create: vi.fn(), @@ -41,8 +38,7 @@ describe('CreateTransactionUseCase', () => { commodityRepository as unknown as CommodityRepositoryInterface, ); - const transactionData = { - currencyCode: 'USD', + const transactionRawData = { description: 'Test transaction', postingDate: '2024-01-01', transactionDate: '2024-01-01', @@ -55,33 +51,29 @@ describe('CreateTransactionUseCase', () => { beforeAll(async () => { user = await createUser(); - - commodity = Commodity.create( - user, - Name.create('Test Commodity'), - CommodityCode.create('TEST'), - 2, - null, - ); }); beforeEach(() => { vi.resetAllMocks(); transactionManager.run.mockImplementation((cb: () => unknown) => cb()); - commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); }); describe('execute', () => { it('should create a new transaction with entries successfully', async () => { - const { transactionContext, transactionDTO } = TransactionBuilder.request( - { - accounts: ['USD'], - commodity, - operations: operationsData, - settings: transactionData, - user, - }, + const { + transactionContext, + transactionData: { transactionCommodity }, + transactionDTO, + } = TransactionBuilder.request({ + currencies: ['USD'], + operations: operationsData, + settings: transactionRawData, + user, + }); + + commodityRepository.getById.mockResolvedValue( + transactionCommodity.toSnapshot(), ); transactionContextLoader.loadContext.mockResolvedValue( @@ -93,8 +85,8 @@ describe('CreateTransactionUseCase', () => { transactionDTO, ); - expect(result.postingDate).toBe(transactionData.postingDate); - expect(result.transactionDate).toBe(transactionData.transactionDate); + expect(result.postingDate).toBe(transactionRawData.postingDate); + expect(result.transactionDate).toBe(transactionRawData.transactionDate); transactionDTO.operations.forEach((operation, index) => { const matchedOperation = result.operations[index]; @@ -117,18 +109,18 @@ describe('CreateTransactionUseCase', () => { expect(savedUserId).toBe(user.getId().valueOf()); expect(savedTransaction.getPostingDate().valueOf()).toBe( - transactionData.postingDate, + transactionRawData.postingDate, ); expect(savedTransaction.getTransactionDate().valueOf()).toBe( - transactionData.transactionDate, + transactionRawData.transactionDate, ); expect(savedTransaction.getOperations().length).toBe( transactionDTO.operations.length, ); - expect(savedTransaction.description).toBe(transactionData.description); + expect(savedTransaction.description).toBe(transactionRawData.description); savedTransaction.getOperations().forEach((operation, index) => { const dtoOperation = transactionDTO.operations[index]; @@ -147,7 +139,6 @@ describe('CreateTransactionUseCase', () => { expect(result.id).toBe(savedTransaction.getId().valueOf()); expect(result.userId).toBe(user.getId().valueOf()); - expect(result.currency).toBe(transactionData.currencyCode); }); it('should propagate error when loadContext fails', async () => { @@ -155,10 +146,9 @@ describe('CreateTransactionUseCase', () => { transactionContextLoader.loadContext.mockRejectedValue(error); const { transactionDTO } = TransactionBuilder.request({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [], - settings: transactionData, + settings: transactionRawData, user, }); @@ -171,14 +161,19 @@ describe('CreateTransactionUseCase', () => { const dbError = new Error('Database error'); mockTransactionRepository.create.mockRejectedValue(dbError); - const { transactionContext, transactionDTO } = TransactionBuilder.request( - { - accounts: ['USD'], - commodity, - operations: operationsData, - settings: transactionData, - user, - }, + const { + transactionContext, + transactionData: { transactionCommodity }, + transactionDTO, + } = TransactionBuilder.request({ + currencies: ['USD'], + operations: operationsData, + settings: transactionRawData, + user, + }); + + commodityRepository.getById.mockResolvedValue( + transactionCommodity.toSnapshot(), ); transactionContextLoader.loadContext.mockResolvedValue( @@ -196,20 +191,25 @@ describe('CreateTransactionUseCase', () => { { accountKey: 'USD', amount: '5000', description: '2' }, ]; - const { transactionContext, transactionDTO } = TransactionBuilder.request( - { - accounts: ['USD'], - commodity, - operations: unbalancedOperations, - settings: transactionData, - user, - }, - ); + const { + transactionContext, + transactionData: { transactionCommodity }, + transactionDTO, + } = TransactionBuilder.request({ + currencies: ['USD'], + operations: unbalancedOperations, + settings: transactionRawData, + user, + }); transactionContextLoader.loadContext.mockResolvedValue( transactionContext, ); + commodityRepository.getById.mockResolvedValue( + transactionCommodity.toSnapshot(), + ); + await expect( createTransactionUseCase.execute(user, transactionDTO), ).rejects.toThrow(UnbalancedTransactionError); diff --git a/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts index 89ce82e9..6a50995e 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/deleteTransaction.test.ts @@ -7,7 +7,7 @@ import type { TransactionManagerInterface, TransactionRepositoryInterface, } from 'src/application/interfaces'; -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils/testEntityBuilder'; import { Transaction, User } from 'src/domain'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -41,11 +41,9 @@ describe('DeleteTransactionUseCase', () => { beforeEach(() => { vi.clearAllMocks(); - const commodity = createCommodity(user); const data = TransactionBuilder.transaction({ - accounts: ['USD', 'EUR'], - commodity, + currencies: ['USD', 'EUR'], operations: [ { accountKey: 'USD', diff --git a/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts index 3976c2c8..9457fd5a 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts @@ -9,13 +9,7 @@ import { OperationReadModel, TransactionReadModel, } from 'src/application/read-models'; -import { - Amount, - Currency, - DateValue, - Id, - Timestamp, -} from 'src/domain/domain-core'; +import { Amount, DateValue, Id, Timestamp } from 'src/domain/domain-core'; import { ForbiddenAccessError } from 'src/infrastructure/errors'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -64,7 +58,6 @@ describe('GetAllTransactionsUseCase', () => { return { createdAt: Timestamp.create().valueOf(), - currency: Currency.create('USD').valueOf(), description: 'Test transaction', id: transactionId, operations: [buildOperation(transactionId, userId)], diff --git a/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts index fe313ce3..e62b5f6e 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts @@ -6,13 +6,7 @@ import { TransactionResponseDTO, } from 'src/application'; import { EntityNotFoundError } from 'src/application/application.errors'; -import { - Amount, - Currency, - DateValue, - Id, - Timestamp, -} from 'src/domain/domain-core'; +import { Amount, DateValue, Id, Timestamp } from 'src/domain/domain-core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { GetTransactionByIdUseCase } from '../GetTransactionById'; @@ -66,7 +60,6 @@ describe('GetTransactionByIdUseCase', () => { const mockTransactionData: TransactionReadModel = { createdAt, - currency: Currency.create('USD').valueOf(), description: 'Test Transaction', id: transactionId, operations, @@ -93,7 +86,6 @@ describe('GetTransactionByIdUseCase', () => { expect(Object.keys(transaction).sort()).toEqual( [ 'createdAt', - 'currency', 'description', 'id', 'operations', diff --git a/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts index 4f02c03d..f789c571 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/updateTransaction.test.ts @@ -15,7 +15,7 @@ import type { TransactionRepositoryInterface, } from 'src/application/interfaces'; import { TransactionContextLoader } from 'src/application/services/TransactionService/transaction.context-loader'; -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { compareEntities, TransactionBuilder } from 'src/db/test-utils'; import { Transaction, User } from 'src/domain'; import { Amount, Id, Version } from 'src/domain/domain-core'; @@ -145,11 +145,9 @@ describe('UpdateTransactionUseCase', () => { beforeEach(() => { vi.clearAllMocks(); - const commodity = createCommodity(user); const data = TransactionBuilder.transaction({ - accounts: ['USD', 'EUR'], - commodity, + currencies: ['USD', 'EUR'], operations: [ { accountKey: 'USD', diff --git a/apps/backend/src/db/createTestUser.ts b/apps/backend/src/db/createTestUser.ts index edda0a53..4399d6c2 100644 --- a/apps/backend/src/db/createTestUser.ts +++ b/apps/backend/src/db/createTestUser.ts @@ -4,12 +4,10 @@ import { Email, Password, Amount, - Currency, CommodityCode, } from 'src/domain/domain-core'; import { User } from 'src/domain/users/user.entity'; -// TODO: move to the test builder export const createUser = async ( params: { name?: string; @@ -58,7 +56,6 @@ export const createAccount = ( user: User, params: { - currency?: Currency; description?: string; name?: string; commodity?: Commodity; @@ -72,7 +69,6 @@ export const createAccount = ( Name.create(params.name ?? 'Test Account'), params.description ?? 'Account for testing', Amount.create('0'), - params.currency ?? Currency.create('USD'), AccountType.create('asset'), ); }; diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts index a8bac057..9dd52368 100644 --- a/apps/backend/src/db/schema.ts +++ b/apps/backend/src/db/schema.ts @@ -1,6 +1,5 @@ export * from './schemas/accounts'; export * from './schemas/transactions'; -export * from './schemas/currencies'; export * from './schemas/operations'; export * from './schemas/users'; export * from './schemas/settings'; diff --git a/apps/backend/src/db/schemas/accounts.ts b/apps/backend/src/db/schemas/accounts.ts index 02119ae6..27163f8b 100644 --- a/apps/backend/src/db/schemas/accounts.ts +++ b/apps/backend/src/db/schemas/accounts.ts @@ -1,5 +1,5 @@ import { ACCOUNT_TYPE_VALUES } from '@ledgerly/shared/constants'; -import { CurrencyCode, UUID } from '@ledgerly/shared/types'; +import { UUID } from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel } from 'drizzle-orm'; import { sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; @@ -23,7 +23,6 @@ export const accountsTable = sqliteTable( .references(() => commoditiesTable.id) .$type(), createdAt, - currency: text('currency').notNull().$type(), currentClearedBalanceLocal: getAmountColumn( 'current_cleared_balance_local', ), @@ -51,10 +50,3 @@ export type AccountDbRow = InferSelectModel; export type AccountDbInsert = InferInsertModel; export type AccountRepoInsert = AccountDbInsert; - -export type AccountDbUpdate = Partial< - Omit< - AccountDbRow, - 'id' | 'userId' | 'createdAt' | 'updatedAt' | 'isTombstone' - > ->; diff --git a/apps/backend/src/db/schemas/currencies.ts b/apps/backend/src/db/schemas/currencies.ts deleted file mode 100644 index 8886eeda..00000000 --- a/apps/backend/src/db/schemas/currencies.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; -import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; - -export const currenciesTable = sqliteTable('currencies', { - code: text('code').notNull().primaryKey().$type(), - name: text('name').notNull(), - symbol: text('symbol').notNull(), -}); diff --git a/apps/backend/src/db/schemas/index.ts b/apps/backend/src/db/schemas/index.ts index de403604..d5ada348 100644 --- a/apps/backend/src/db/schemas/index.ts +++ b/apps/backend/src/db/schemas/index.ts @@ -1,5 +1,4 @@ export { accountsTable } from './accounts'; -export { currenciesTable } from './currencies'; export { operationsTable, operationsRelations } from './operations'; export { transactionsTable, transactionsRelations } from './transactions'; export { settingsTable } from './settings'; diff --git a/apps/backend/src/db/schemas/settings.ts b/apps/backend/src/db/schemas/settings.ts index 898fe897..86d6c6fa 100644 --- a/apps/backend/src/db/schemas/settings.ts +++ b/apps/backend/src/db/schemas/settings.ts @@ -1,14 +1,9 @@ import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { createdAt, updatedAt } from './common'; -import { currenciesTable } from './currencies'; import { usersTable } from './users'; export const settingsTable = sqliteTable('settings', { - baseCurrency: text('base_currency') - .notNull() - .default('RUB') - .references(() => currenciesTable.code), createdAt, updatedAt, userId: text('user_id') diff --git a/apps/backend/src/db/schemas/transactions.ts b/apps/backend/src/db/schemas/transactions.ts index 3c3b0f04..5f63bfc8 100644 --- a/apps/backend/src/db/schemas/transactions.ts +++ b/apps/backend/src/db/schemas/transactions.ts @@ -1,4 +1,4 @@ -import { CurrencyCode, UUID } from '@ledgerly/shared/types'; +import { UUID } from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm'; import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'; @@ -23,7 +23,6 @@ export const transactionsTable = sqliteTable( .references(() => commoditiesTable.id) .$type(), createdAt, - currency: text('currency').notNull().$type(), description, id, isTombstone, diff --git a/apps/backend/src/db/scripts/currenciesSeed.ts b/apps/backend/src/db/scripts/currenciesSeed.ts deleted file mode 100644 index 4e058330..00000000 --- a/apps/backend/src/db/scripts/currenciesSeed.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; -import { DataBase } from 'src/db'; - -import { db } from '../index'; -import { currenciesTable } from '../schemas'; - -type Currency = { - code: CurrencyCode; - name: string; - symbol: string; -}; - -const defaultCurrencies: Currency[] = [ - { code: 'USD' as CurrencyCode, name: 'US Dollar', symbol: '$' }, - { code: 'EUR' as CurrencyCode, name: 'Euro', symbol: '€' }, - { code: 'GBP' as CurrencyCode, name: 'British Pound', symbol: '£' }, - { code: 'JPY' as CurrencyCode, name: 'Japanese Yen', symbol: '¥' }, - { code: 'CHF' as CurrencyCode, name: 'Swiss Franc', symbol: 'CHF' }, - { code: 'CAD' as CurrencyCode, name: 'Canadian Dollar', symbol: 'C$' }, -]; - -// TODO: fix dataBase: DataBase = db as unknown as DataBase -export const seedCurrencies = async ( - dataBase: DataBase = db as unknown as DataBase, -) => { - // console.info('🌱 Starting currency seeding...'); - - try { - // ✅ Используем Drizzle ORM с ON CONFLICT - for (const currency of defaultCurrencies) { - await dataBase.insert(currenciesTable).values(currency); - - // console.info(`✅ Currency ${currency.code} (${currency.name}) seeded`); - } - - // console.info('🎉 Currency seeding completed successfully!'); - } catch (error) { - console.error('❌ Error seeding currencies:', error); - throw error; - } -}; - -// ✅ Для прямого запуска скрипта -// if (import.meta.url === `file://${process.argv[1]}`) { -// const databaseUrl = process.env.DATABASE_URL || 'file:./dev.db'; - -// seedCurrencies(databaseUrl) -// .then(() => { -// console.info('🏁 Script completed'); -// process.exit(0); -// }) -// .catch((error) => { -// console.error('💥 Script failed:', error); -// process.exit(1); -// }); -// } diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index c91d9823..7f413946 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -5,7 +5,6 @@ import { ACCOUNT_TYPES } from '@ledgerly/shared/constants'; import { dateInIsoFormat } from '@ledgerly/shared/libs'; import { AccountTypeValue, - CurrencyCode, IsoDateString, AmountString, UUID, @@ -19,17 +18,11 @@ import { eq, sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/libsql'; import { migrate } from 'drizzle-orm/libsql/migrator'; import { DataBase } from 'src/db'; -import { - Amount, - CommodityCode, - Currency, - DateValue, -} from 'src/domain/domain-core'; +import { Amount, CommodityCode, DateValue } from 'src/domain/domain-core'; import { OperationSnapshot } from 'src/domain/operations/types'; import { TransactionSnapshot } from 'src/domain/transactions/types'; import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; import { CommodityPersistenceMapper } from 'src/infrastructure/db/commodities/commodity-persistence.mapper'; -import { AmountFormatter } from 'src/presentation/formatters'; import { TransactionDbInsert, @@ -82,7 +75,6 @@ export type CreateTransactionProps = { description?: string; postingDate?: IsoDateString; transactionDate?: IsoDateString; - currencyCode?: CurrencyCode; isTombstone?: boolean; }; @@ -97,7 +89,6 @@ export type TransactionOperationSeed = { }; export type TransactionSeed = { - currencyCode?: CurrencyCode; description: string; operations: TransactionOperationSeed[]; postingDate?: IsoDateString; @@ -229,7 +220,6 @@ export class TestDB { const transactionData: TransactionDbInsert = { ...TestDB.uuid, ...TestDB.createTimestamps, - currency: params?.currencyCode ?? Currency.create('USD').valueOf(), description: params?.description ?? `Test Transaction ${this.transactionCounter.getNextName()}`, @@ -258,7 +248,6 @@ export class TestDB { description?: string; postingDate?: IsoDateString; transactionDate?: IsoDateString; - currencyCode?: CurrencyCode; isTombstone?: boolean; operations: { accountId: UUID; @@ -296,7 +285,6 @@ export class TestDB { userId: UUID, commodityId: UUID, { - currencyCode = 'USD' as CurrencyCode, description, isTombstone = false, operations, @@ -305,7 +293,6 @@ export class TestDB { }: TransactionSeed, ): Promise => { return this.createTransactionWithOperations(userId, commodityId, { - currencyCode, description, isTombstone, operations: operations.map((operation) => ({ @@ -388,40 +375,6 @@ export class TestDB { return { operations, ...transaction }; }; - getTransactionInPTAFormat = async ( - transactionId: UUID, - ): Promise => { - const transaction = await this.getTransactionById(transactionId); - if (!transaction) return null; - - const operations = await this.db.select().from(schema.operationsTable); - - // Format in PTA (Plain Text Accounting) style - let output = `${transaction.transactionDate} ${transaction.description}\n`; - - for (const operation of operations) { - const account = await this.db - .select() - .from(schema.accountsTable) - .where(sql`${schema.accountsTable.id} = ${operation.accountId}`) - .get(); - - if (!account) continue; - - const formatter = new AmountFormatter(); - - const amount = Amount.restore(operation.amount); - const userFriendlyAmount = formatter.formatForTable(amount, 'en-US'); - const accountName = account.name; - const currency = account.currency; - const systemMarker = operation.isSystem ? ' [system]' : ''; - - output += ` ${accountName.padEnd(40)} ${operation.description} ${userFriendlyAmount} ${currency}${systemMarker}\n`; - } - - return output; - }; - softDeleteTransaction = async (transactionId: UUID) => { return await this.db .update(schema.transactionsTable) @@ -476,7 +429,6 @@ export class TestDB { commodityId: UUID, params?: { name?: string; - currency?: CurrencyCode; type?: AccountTypeValue; initialBalance?: AmountString; description?: string; @@ -484,7 +436,6 @@ export class TestDB { }, ) => { const accountData = { - currency: 'USD' as unknown as CurrencyCode, description: '', initialBalance: Amount.create('0').valueOf(), isSystem: false, @@ -498,7 +449,6 @@ export class TestDB { .insert(accountsTable) .values({ commodityId, - currency: accountData.currency, currentClearedBalanceLocal: accountData.initialBalance ?? 0, description: accountData.description || '', initialBalance: accountData.initialBalance ?? 0, @@ -633,17 +583,14 @@ export class TestDB { }); const accountUSD1 = await this.createAccount(user.id, usdCommodity.id, { - currency: Currency.create('USD').valueOf(), name: 'Savings Account USD', }); const accountUSD2 = await this.createAccount(user.id, usdCommodity.id, { - currency: Currency.create('USD').valueOf(), name: 'Checking Account USD', }); const accountEUR = await this.createAccount(user.id, eurCommodity.id, { - currency: Currency.create('EUR').valueOf(), name: 'Credit Card EUR', }); diff --git a/apps/backend/src/db/test-utils/builder.test.ts b/apps/backend/src/db/test-utils/builder.test.ts index 5b758ac6..315f6fe2 100644 --- a/apps/backend/src/db/test-utils/builder.test.ts +++ b/apps/backend/src/db/test-utils/builder.test.ts @@ -1,23 +1,22 @@ -import { createCommodity, createUser } from 'src/db/createTestUser'; -import { Commodity, User } from 'src/domain'; +import { createUser } from 'src/db/createTestUser'; +import { User } from 'src/domain'; import { InsufficientOperationsError } from 'src/domain/domain.errors'; import { beforeAll, describe, expect, it } from 'vitest'; +import { EntityNotFoundError } from '../../application/application.errors'; + import { TransactionBuilder } from './testEntityBuilder'; describe('TransactionBuilder', () => { let user: User; - let commodity: Commodity; beforeAll(async () => { user = await createUser(); - commodity = createCommodity(user); }); it('should be implemented', () => { TransactionBuilder.transaction({ - accounts: ['USD', 'EUR'], - commodity: commodity, + currencies: ['USD', 'EUR'], operations: [ { accountKey: 'USD', amount: '10000', description: '1' }, { @@ -32,8 +31,7 @@ describe('TransactionBuilder', () => { it('builds an invalid request fixture without creating a domain transaction', () => { const fixture = TransactionBuilder.request({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [{ accountKey: 'USD', amount: '0' }], user, }); @@ -44,11 +42,23 @@ describe('TransactionBuilder', () => { it('applies domain invariants when building a transaction', () => { expect(() => TransactionBuilder.transaction({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [{ accountKey: 'USD', amount: '0' }], user, }), ).toThrow(InsufficientOperationsError); }); + + it('throws an error if an operation references a non-existent account', () => { + expect(() => + TransactionBuilder.transaction({ + currencies: ['USD'], + operations: [ + { accountKey: 'USD', amount: '100', description: 'Debit' }, + { accountKey: 'EUR', amount: '-100', description: 'Credit' }, + ], + user, + }), + ).toThrow(EntityNotFoundError); + }); }); diff --git a/apps/backend/src/db/test-utils/prettyPrint.test.ts b/apps/backend/src/db/test-utils/prettyPrint.test.ts index 572206fe..7c1e069e 100644 --- a/apps/backend/src/db/test-utils/prettyPrint.test.ts +++ b/apps/backend/src/db/test-utils/prettyPrint.test.ts @@ -1,5 +1,5 @@ -import { createCommodity, createUser } from 'src/db/createTestUser'; -import { Commodity, User } from 'src/domain'; +import { createUser } from 'src/db/createTestUser'; +import { User } from 'src/domain'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { prettyPrint } from './prettyPrint'; @@ -7,7 +7,6 @@ import { TransactionBuilder } from './testEntityBuilder'; describe('prettyPrint', () => { let user: User; - let commodity: Commodity; afterEach(() => { vi.restoreAllMocks(); @@ -15,13 +14,11 @@ describe('prettyPrint', () => { beforeAll(async () => { user = await createUser(); - commodity = createCommodity(user); }); it('prints an operation in PTA format', () => { const data = TransactionBuilder.transaction({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [ { accountKey: 'USD', @@ -43,6 +40,7 @@ describe('prettyPrint', () => { const result = prettyPrint.operationPTA( data.operations[0], data.accountsMap, + data.commoditiesMapById, ); expect(result).toBeUndefined(); @@ -54,8 +52,7 @@ describe('prettyPrint', () => { it('prints a transaction in PTA format', () => { const data = TransactionBuilder.transaction({ - accounts: ['USD', 'EUR'], - commodity: commodity, + currencies: ['USD', 'EUR'], operations: [ { accountKey: 'USD', @@ -77,6 +74,7 @@ describe('prettyPrint', () => { const result = prettyPrint.transactionPTA( data.transaction, data.accountsMap, + data.commoditiesMapById, ); expect(result).toBeUndefined(); diff --git a/apps/backend/src/db/test-utils/prettyPrint.ts b/apps/backend/src/db/test-utils/prettyPrint.ts index 166b064c..43113656 100644 --- a/apps/backend/src/db/test-utils/prettyPrint.ts +++ b/apps/backend/src/db/test-utils/prettyPrint.ts @@ -1,6 +1,6 @@ // Format and print transactions in PTA format import { UUID } from '@ledgerly/shared/types'; -import { Account, Operation, Transaction } from 'src/domain'; +import { Account, Commodity, Operation, Transaction } from 'src/domain'; import { Amount } from 'src/domain/domain-core'; import { AmountFormatter } from 'src/presentation/formatters'; const formatter = new AmountFormatter(); @@ -20,29 +20,38 @@ const formatDebitCredit = ( const getAccountInfo = ( accountId: UUID, - accountMap?: Map, -): { currency: string; name: string } => { + accountMap: Map = new Map(), + commoditiesMapById: Map = new Map(), +): { commodity: string; name: string } => { if (!accountMap) { - return { currency: 'N/A', name: 'Unknown Account' }; + return { commodity: 'N/A', name: 'Unknown Account' }; } const account = accountMap.get(accountId); if (!account) { - return { currency: 'N/A', name: 'Unknown Account' }; + return { commodity: 'N/A', name: 'Unknown Account' }; } - return { currency: account.currency.valueOf(), name: account.name.valueOf() }; + + const commodity = commoditiesMapById.get(account.toSnapshot().commodityId); + + return { + commodity: commodity?.toSnapshot().code ?? 'N/A', + name: account.name.valueOf(), + }; }; function formatOperationPTA( operation: Operation, accountMap?: Map, + commoditiesMapById?: Map, ): string { - const { currency, name } = getAccountInfo( + const { commodity, name } = getAccountInfo( operation.getAccountId().valueOf(), accountMap, + commoditiesMapById, ); const { credit, debit } = formatDebitCredit(operation.amount); - return ` ${name.padEnd(20)} ${operation.description.padEnd(50)} ${currency.padEnd(8)} ${debit} ${credit}`; + return ` ${name.padEnd(20)} ${operation.description.padEnd(50)} ${commodity.padEnd(8)} ${debit} ${credit}`; } /** @@ -57,6 +66,7 @@ function formatOperationPTA( function formatTransactionPTA( transaction: Transaction, accountMap?: Map, + commoditiesMapById?: Map, ): string { const { credit, debit } = transaction.getOperations().reduce( (totals, operation) => { @@ -73,6 +83,11 @@ function formatTransactionPTA( debit: Amount.create('0'), }, ); + + const commodity = + commoditiesMapById?.get(transaction.toSnapshot().commodityId)?.toSnapshot() + .code ?? 'N/A'; + const lines: string[] = []; lines.push( `${transaction.getTransactionDate().valueOf()} ${transaction.description}`, @@ -81,10 +96,10 @@ function formatTransactionPTA( ` ${'Account'.padEnd(20)} ${'Description'.padEnd(50)} ${'Currency'.padEnd(8)} ${'Debit'.padStart(amountColumnWidth)} ${'Credit'.padStart(amountColumnWidth)}`, ); transaction.getOperations().forEach((operation) => { - lines.push(formatOperationPTA(operation, accountMap)); + lines.push(formatOperationPTA(operation, accountMap, commoditiesMapById)); }); lines.push( - ` ${'Total'.padEnd(20)} ${''.padEnd(50)} ${transaction.currency.valueOf().padEnd(8)} ${formatAmountColumn(debit)} ${formatAmountColumn(credit)}`, + ` ${'Total'.padEnd(20)} ${''.padEnd(50)} ${commodity.padEnd(8)} ${formatAmountColumn(debit)} ${formatAmountColumn(credit)}`, ); return lines.join('\n'); } @@ -92,15 +107,19 @@ function formatTransactionPTA( function printOperationPTA( operation: Operation, accountMap?: Map, + commoditiesMapById?: Map, ): void { - console.info(formatOperationPTA(operation, accountMap)); + console.info(formatOperationPTA(operation, accountMap, commoditiesMapById)); } function printTransactionPTA( transaction: Transaction, accountMap?: Map, + commoditiesMapById?: Map, ): void { - console.info(formatTransactionPTA(transaction, accountMap)); + console.info( + formatTransactionPTA(transaction, accountMap, commoditiesMapById), + ); } export const prettyPrint = { diff --git a/apps/backend/src/db/test-utils/testEntityBuilder.ts b/apps/backend/src/db/test-utils/testEntityBuilder.ts index 90079a9d..9d6abd36 100644 --- a/apps/backend/src/db/test-utils/testEntityBuilder.ts +++ b/apps/backend/src/db/test-utils/testEntityBuilder.ts @@ -1,6 +1,5 @@ import { UUID, - CurrencyCode, IsoDateString, CommodityCodeString, } from '@ledgerly/shared/types'; @@ -17,13 +16,7 @@ import { Transaction, User, } from 'src/domain'; -import { - Amount, - CommodityCode, - Currency, - DateValue, - Name, -} from 'src/domain/domain-core'; +import { Amount, CommodityCode, DateValue, Name } from 'src/domain/domain-core'; import { CreateTransactionProps, TransactionBuildContext, @@ -42,26 +35,25 @@ export type TransactionProps = { postingDate?: string; transactionDate?: string; description?: string; - currencyCode: string; + currency: string; }; export type TransactionBuilderOptions = { user: User; - commodity: Commodity; - accounts: string[]; operations: OperationDataForTransaction[]; settings?: Partial; + currencies: string[]; }; export type TransactionRequestBuilderResult = { accountsMap: Map; + commoditiesMap: Map; + commoditiesMapById: Map; accounts: Account[]; commodities: Commodity[]; transactionContext: TransactionBuildContext; getAccountByKey: (key: string) => Account; getCommodityByKey: (code: string) => Commodity; - getSystemAccountByCurrency: (currency: string) => Account; - systemAccounts: Map; user: User; transactionDTO: CreateTransactionRequestDTO; transactionData: { @@ -70,6 +62,7 @@ export type TransactionRequestBuilderResult = { transactionDate: IsoDateString; userId: UUID; commodityId: UUID; + transactionCommodity: Commodity; }; operationsData: OperationDataForTransaction[]; }; @@ -87,13 +80,15 @@ export class TransactionBuilder { private readonly user: User; private readonly accounts = new Map(); private readonly accountsMap = new Map(); - private readonly commoditiesMap = new Map(); - private readonly systemAccounts = new Map(); + private readonly commoditiesMapByCommodityCode = new Map< + CommodityCodeString, + Commodity + >(); + private readonly commoditiesMapById = new Map(); private readonly operationsData: OperationDataForTransaction[]; private readonly postingDate: IsoDateString; private readonly transactionDate: IsoDateString; private readonly description: string; - private readonly transactionCurrency: Currency; private readonly transactionCommodity: Commodity; private constructor(options: TransactionBuilderOptions) { @@ -111,17 +106,20 @@ export class TransactionBuilder { this.description = options.settings?.description ?? 'Test Transaction'; - this.transactionCurrency = Currency.create( - options.settings?.currencyCode ?? 'USD', - ); + this.createAccounts(options.currencies); + this.validateAccountKeys(); + + const transactionCommodityCode = options.settings?.currency ?? 'USD'; - this.transactionCommodity = Commodity.create( - this.user, - Name.create(`Commodity ${options.settings?.currencyCode ?? 'USD'}`), - CommodityCode.create(options.settings?.currencyCode ?? 'USD'), + const transactionCommodity = this.commoditiesMapByCommodityCode.get( + transactionCommodityCode as CommodityCodeString, ); - this.createAccounts(options.accounts); + if (!transactionCommodity) { + throw new EntityNotFoundError({ entityType: Commodity.entityType }); + } + + this.transactionCommodity = transactionCommodity; } static request( @@ -142,37 +140,44 @@ export class TransactionBuilder { return new TransactionBuilder(options).buildPersistence(); } + private validateAccountKeys(): void { + const invalidKeys = this.operationsData.filter( + (operation) => !this.accounts.has(operation.accountKey), + ); + + if (invalidKeys.length > 0) { + throw new EntityNotFoundError({ entityType: Account.entityType }); + } + } + private createAccounts(currencyCodes: string[]): void { - currencyCodes.forEach((currencyCode) => { - const code = CommodityCode.create(currencyCode); + currencyCodes.forEach((commodityCode) => { + const code = CommodityCode.create(commodityCode); const commodity = Commodity.create( this.user, - Name.create(`Commodity ${currencyCode}`), + Name.create(`Commodity ${commodityCode}`), code, ); const account = Account.create( this.user, commodity, - Name.create(`Account ${currencyCode}`), - `Account ${currencyCode}`, + Name.create(`Account ${commodityCode}`), + `Account ${commodityCode}`, Amount.create('0'), - Currency.create(currencyCode), AccountType.create('asset'), ); - this.accounts.set(currencyCode, account); + this.accounts.set(commodityCode, account); this.accountsMap.set(account.getId().valueOf(), account); - this.commoditiesMap.set(code.valueOf(), commodity); + this.commoditiesMapByCommodityCode.set(code.valueOf(), commodity); + this.commoditiesMapById.set(commodity.getId().valueOf(), commodity); }); - - this.createSystemAccounts(); } - private getCommodityByCurrency(currency: string): Commodity { - const code = CommodityCode.create(currency); - const commodity = this.commoditiesMap.get(code.valueOf()); + private getCommodityById(commodityId: UUID): Commodity { + const commodity = this.commoditiesMapById.get(commodityId); if (!commodity) { throw new EntityNotFoundError({ entityType: Commodity.entityType }); @@ -181,25 +186,6 @@ export class TransactionBuilder { return commodity; } - private createSystemAccounts(): void { - this.accounts.forEach((account) => { - const currencyCode = account.currency.valueOf(); - const commodity = this.getCommodityByCurrency(currencyCode); - const systemAccount = Account.create( - this.user, - commodity, - Name.create(`System Account ${currencyCode}`), - `System Account for ${currencyCode}`, - Amount.create('0'), - Currency.create(currencyCode), - AccountType.create('currencyTrading'), - ); - - this.systemAccounts.set(currencyCode, systemAccount); - this.accountsMap.set(systemAccount.getId().valueOf(), systemAccount); - }); - } - getAccountByKey(key: string): Account { const account = this.accounts.get(key); @@ -210,18 +196,10 @@ export class TransactionBuilder { return account; } - getSystemAccountByCurrency(currency: string): Account { - const account = this.systemAccounts.get(currency as CurrencyCode); - - if (!account) { - throw new EntityNotFoundError({ entityType: Account.entityType }); - } - - return account; - } - getCommodityByKey(code: string): Commodity { - const commodity = this.commoditiesMap.get(code as CommodityCodeString); + const commodity = this.commoditiesMapByCommodityCode.get( + code as CommodityCodeString, + ); if (!commodity) { throw new EntityNotFoundError({ entityType: Commodity.entityType }); @@ -248,12 +226,7 @@ export class TransactionBuilder { private buildTransactionProps(): CreateTransactionProps { return { - commodityId: this.getCommodityByCurrency( - this.transactionCurrency.valueOf(), - ) - .getId() - .valueOf(), - currency: this.transactionCurrency, + commodityId: this.transactionCommodity.getId().valueOf(), description: this.description, operations: this.operationsData.map((operation) => { const amount = Amount.create(operation.amount); @@ -274,34 +247,30 @@ export class TransactionBuilder { return { accounts: Array.from(this.accountsMap.values()), accountsMap: this.accountsMap, - commodities: Array.from(this.commoditiesMap.values()), + commodities: Array.from(this.commoditiesMapByCommodityCode.values()), + commoditiesMap: this.commoditiesMapByCommodityCode, + commoditiesMapById: this.commoditiesMapById, getAccountByKey: this.getAccountByKey.bind(this), getCommodityByKey: this.getCommodityByKey.bind(this), - getSystemAccountByCurrency: this.getSystemAccountByCurrency.bind(this), operationsData: this.operationsData, - systemAccounts: this.systemAccounts, transactionContext: { accountsMap: this.accountsMap, - systemAccountsMap: this.systemAccounts, }, + transactionData: { - commodityId: this.getCommodityByCurrency( - this.transactionCurrency.valueOf(), - ) - .getId() - .valueOf(), + commodityId: this.transactionCommodity.getId().valueOf(), description: this.description, postingDate: this.postingDate, + transactionCommodity: this.transactionCommodity, transactionDate: this.transactionDate, userId: this.user.getId().valueOf(), }, transactionDTO: { - commodityId: this.getCommodityByCurrency( - this.transactionCurrency.valueOf(), + commodityId: this.getCommodityById( + this.transactionCommodity.getId().valueOf(), ) .getId() .valueOf(), - currencyCode: this.transactionCurrency.valueOf(), description: this.description, operations: this.buildOperationsDTO(), postingDate: this.postingDate, @@ -314,8 +283,8 @@ export class TransactionBuilder { private buildTransaction(): TransactionBuilderResult { const requestFixture = this.buildRequest(); - const commodity = this.getCommodityByCurrency( - requestFixture.transactionDTO.currencyCode, + const commodity = this.getCommodityById( + requestFixture.transactionDTO.commodityId, ); const transaction = Transaction.create( diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index eb9d81f0..6c367158 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -17,7 +17,6 @@ import { TransactionRepository, TransactionQueryRepository, OperationRepository, - CurrencyRepository, TransactionManager, AccountRepository, UserRepository, @@ -37,7 +36,6 @@ export const createContainer = (db: DataBase): AppContainer => { const transactionManager = new TransactionManager(db); const accountRepository = new AccountRepository(transactionManager); - const currencyRepository = new CurrencyRepository(transactionManager); const commodityRepository = new CommodityRepository(transactionManager); const operationRepository = new OperationRepository(transactionManager); const transactionRepository = new TransactionRepository( @@ -54,7 +52,6 @@ export const createContainer = (db: DataBase): AppContainer => { const repositories: AppContainer['repositories'] = { account: accountRepository, commodity: commodityRepository, - currency: currencyRepository, transaction: transactionRepository, transactionQuery: transactionQueryRepository, user: userRepository, diff --git a/apps/backend/src/di/types.ts b/apps/backend/src/di/types.ts index 482e1adb..d2b5613d 100644 --- a/apps/backend/src/di/types.ts +++ b/apps/backend/src/di/types.ts @@ -16,7 +16,6 @@ import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; import { TransactionQueryRepository, AccountRepository, - CurrencyRepository, TransactionRepository, CommodityRepository, UserRepository, @@ -29,7 +28,6 @@ import { } from 'src/presentation/http'; type Repositories = { - currency: CurrencyRepository; transaction: TransactionRepository; commodity: CommodityRepository; transactionQuery: TransactionQueryRepository; diff --git a/apps/backend/src/domain/accounts/account.entity.ts b/apps/backend/src/domain/accounts/account.entity.ts index 67a7823e..6669a829 100644 --- a/apps/backend/src/domain/accounts/account.entity.ts +++ b/apps/backend/src/domain/accounts/account.entity.ts @@ -1,7 +1,6 @@ import { Commodity } from '../commodities'; import { Amount, - Currency, Id, Timestamp, Name, @@ -29,7 +28,6 @@ export class Account { public description: string, private initialBalance: Amount, private currentClearedBalanceLocal: Amount, - public currency: Currency, private type: AccountType, public isSystem: boolean, ) {} @@ -40,7 +38,6 @@ export class Account { name: Name, description: string, initialBalance: Amount, - currency: Currency, type: AccountType, ): Account { const identity = EntityIdentity.create(); @@ -69,7 +66,6 @@ export class Account { description, initialBalance, Amount.create('0'), - currency, type, isSystem, ); @@ -79,7 +75,6 @@ export class Account { const { commodityId, createdAt, - currency, currentClearedBalanceLocal, description, id, @@ -121,7 +116,6 @@ export class Account { description, Amount.restore(initialBalance), Amount.restore(currentClearedBalanceLocal), - Currency.restore(currency), AccountType.restore(type), isSystem, ); @@ -171,7 +165,6 @@ export class Account { return { commodityId: this.commodityRelation.getParentId().valueOf(), createdAt: this.getCreatedAt().valueOf(), - currency: this.currency.valueOf(), currentClearedBalanceLocal: this.currentClearedBalanceLocal.valueOf(), description: this.description, id: this.getId().valueOf(), @@ -192,24 +185,15 @@ export class Account { update(data: AccountUpdateProps): void { this.validateUpdateIsAllowed(); - const currency = data.currency - ? Currency.create(data.currency) - : this.currency; - const name = data.name ? Name.create(data.name) : this.name; this.description = data.description ?? this.description; this.type = data.type ? AccountType.create(data.type) : this.type; - this.currency = currency; this.name = name; this.touch(); } - isCurrencySame(currency: Currency): boolean { - return this.currency.valueOf() === currency.valueOf(); - } - isCommoditySame(commodity: Commodity): boolean { return this.commodityRelation.getParentId().equals(commodity.getId()); } diff --git a/apps/backend/src/domain/accounts/account.test.ts b/apps/backend/src/domain/accounts/account.test.ts index 728dda4a..bd4aeb4f 100644 --- a/apps/backend/src/domain/accounts/account.test.ts +++ b/apps/backend/src/domain/accounts/account.test.ts @@ -3,7 +3,6 @@ import { AccountType, Commodity } from 'src/domain/'; import { Amount, Name, - Currency, Id, Timestamp, CommodityCode, @@ -16,11 +15,6 @@ import { Account } from './account.entity'; const userTypeValue = 'asset'; -const currencyUSD = Currency.create('USD'); -const currencyEUR = Currency.create('EUR'); - -const currencyCodeEUR = currencyEUR.valueOf(); - const name = Name.create('account-name'); describe('Account Domain Entity', () => { @@ -54,7 +48,6 @@ describe('Account Domain Entity', () => { name, 'account-description', Amount.create('0'), - currencyUSD, accountType, ); @@ -64,11 +57,14 @@ describe('Account Domain Entity', () => { expect(account).toHaveProperty('name', name); expect(account).toHaveProperty('description', 'account-description'); expect(account).toHaveProperty('initialBalance', Amount.create('0')); - expect(account).toHaveProperty('currency', currencyUSD); expect(account.getType().valueOf()).toBe(accountType.valueOf()); expect(account.belongsToUser(userId)).toBe(true); expect(account.getType().equals(accountType)).toBe(true); expect(account.isCommoditySame(commodity)).toBe(true); + expect(account).toHaveProperty( + 'commodityRelation', + expect.objectContaining({ parentId: commodity.getId() }), + ); }); }); @@ -85,7 +81,6 @@ describe('Account Domain Entity', () => { const account = Account.restore({ commodityId: commodity.getId().valueOf(), createdAt: createdAt.valueOf(), - currency: currencyCodeEUR, currentClearedBalanceLocal: Amount.create('500').valueOf(), description: 'restored-description', id: accountId.valueOf(), @@ -104,7 +99,10 @@ describe('Account Domain Entity', () => { expect(account).toHaveProperty('name', Name.create('restored-account')); expect(account).toHaveProperty('description', 'restored-description'); expect(account).toHaveProperty('initialBalance', Amount.create('500')); - expect(account).toHaveProperty('currency', currencyEUR); + expect(account).toHaveProperty( + 'commodityRelation', + expect.objectContaining({ parentId: commodity.getId() }), + ); }); }); @@ -116,7 +114,6 @@ describe('Account Domain Entity', () => { Name.create('initial-name'), 'description', Amount.create('0'), - currencyUSD, accountType, ); @@ -134,7 +131,6 @@ describe('Account Domain Entity', () => { name, 'account-description', Amount.create('0'), - currencyUSD, accountType, ); @@ -152,7 +148,6 @@ describe('Account Domain Entity', () => { name, 'account-description', Amount.create('0'), - currencyUSD, accountType, ); @@ -170,7 +165,6 @@ describe('Account Domain Entity', () => { name, 'account-description', Amount.create('0'), - currencyUSD, accountType, ); @@ -207,7 +201,6 @@ describe('Account Domain Entity', () => { name, 'account-description', Amount.create('0'), - currencyUSD, accountType, ); diff --git a/apps/backend/src/domain/accounts/types.ts b/apps/backend/src/domain/accounts/types.ts index f0ff8730..3a4ca80b 100644 --- a/apps/backend/src/domain/accounts/types.ts +++ b/apps/backend/src/domain/accounts/types.ts @@ -1,6 +1,5 @@ import { AccountTypeValue, - CurrencyCode, IsoDatetimeString, AmountString, UUID, @@ -8,7 +7,6 @@ import { export type AccountSnapshot = { createdAt: IsoDatetimeString; - currency: CurrencyCode; currentClearedBalanceLocal: AmountString; commodityId: UUID; description: string; @@ -23,7 +21,6 @@ export type AccountSnapshot = { }; export type AccountUpdateProps = Partial<{ - currency: CurrencyCode; description: string; name: string; type: AccountTypeValue; diff --git a/apps/backend/src/domain/domain-core/value-objects/Currency.ts b/apps/backend/src/domain/domain-core/value-objects/Currency.ts deleted file mode 100644 index cd602b2a..00000000 --- a/apps/backend/src/domain/domain-core/value-objects/Currency.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CurrencyCode } from '@ledgerly/shared/types'; -import { currencyCode } from '@ledgerly/shared/validation'; - -const SUPPORTED_CODES = new Set([ - 'USD' as CurrencyCode, - 'EUR' as CurrencyCode, - 'GBP' as CurrencyCode, - 'RUB' as CurrencyCode, -]); - -export class Currency { - private readonly code: CurrencyCode; - - private constructor(code: CurrencyCode) { - this.code = code; - Object.freeze(this); - } - - static create(raw: string): Currency { - const normalized = raw.trim().toUpperCase(); - const parsed = currencyCode.parse(normalized); // throws on invalid format - return new Currency(parsed); - } - - static restore(codeStr: string): Currency { - const parsed = currencyCode.parse(codeStr); - return new Currency(parsed); - } - - equals(other: Currency): boolean { - return this.code === other.code; - } - - valueOf(): CurrencyCode { - return this.code; - } - - toPersistence(): CurrencyCode { - return this.code; - } - - static isSupported(codeOrCurrency: string | Currency): boolean { - const code = - typeof codeOrCurrency === 'string' - ? codeOrCurrency.toUpperCase() - : codeOrCurrency.valueOf(); - return SUPPORTED_CODES.has(code as CurrencyCode); - } -} diff --git a/apps/backend/src/domain/domain-core/value-objects/index.ts b/apps/backend/src/domain/domain-core/value-objects/index.ts index 278baf2d..6276c8cc 100644 --- a/apps/backend/src/domain/domain-core/value-objects/index.ts +++ b/apps/backend/src/domain/domain-core/value-objects/index.ts @@ -1,7 +1,6 @@ export { Id } from './Id'; export { Timestamp } from './Timestamp'; export { Amount } from './Amount'; -export { Currency } from './Currency'; export { Name } from './Name'; export { Email } from './Email'; export { ParentChildRelation } from './ParentChildRelation'; diff --git a/apps/backend/src/domain/domain-core/value-objects/value-object-api-conventions.test.ts b/apps/backend/src/domain/domain-core/value-objects/value-object-api-conventions.test.ts index c7bbedf3..d9e30273 100644 --- a/apps/backend/src/domain/domain-core/value-objects/value-object-api-conventions.test.ts +++ b/apps/backend/src/domain/domain-core/value-objects/value-object-api-conventions.test.ts @@ -4,7 +4,6 @@ import { AccountType } from '../../accounts'; import { EntityIdentity, EntityTimestamps, SoftDelete } from '../behaviors'; import { Amount } from './Amount'; -import { Currency } from './Currency'; import { DateValue } from './DateValue'; import { Email } from './Email'; import { Id } from './Id'; @@ -19,7 +18,6 @@ describe('value object API conventions', () => { const id = '11111111-1111-4111-8111-111111111111'; expect(Amount.restore('1200').valueOf()).toBe('1200'); - expect(Currency.restore('USD').valueOf()).toBe('USD'); expect(DateValue.restore('2026-07-10').valueOf()).toBe('2026-07-10'); expect(Email.restore('user@example.com').valueOf()).toBe( 'user@example.com', @@ -41,7 +39,6 @@ describe('value object API conventions', () => { const childId = Id.restore('22222222-2222-4222-8222-222222222222'); expect(Amount.create('1200').equals(Amount.restore('1200'))).toBe(true); - expect(Currency.create('usd').equals(Currency.restore('USD'))).toBe(true); expect( DateValue.restore('2026-07-10').equals(DateValue.restore('2026-07-10')), ).toBe(true); @@ -73,7 +70,6 @@ describe('value object API conventions', () => { it('does not expose domain fromPersistence restoration aliases', () => { const constructors = [ Amount, - Currency, Email, Id, Name, @@ -95,8 +91,6 @@ describe('value object API conventions', () => { const valueObjects = [ Amount.create('1200'), Amount.restore('1200'), - Currency.create('usd'), - Currency.restore('USD'), DateValue.create(), DateValue.restore('2026-07-10'), Email.create('USER@example.com'), diff --git a/apps/backend/src/domain/domain-entity-identity-api.test.ts b/apps/backend/src/domain/domain-entity-identity-api.test.ts index 968c3b50..8c53a8db 100644 --- a/apps/backend/src/domain/domain-entity-identity-api.test.ts +++ b/apps/backend/src/domain/domain-entity-identity-api.test.ts @@ -2,7 +2,6 @@ import { createAccount, createUser } from 'src/db/createTestUser'; import { Account, Operation, Transaction, User } from 'src/domain'; import { Amount, - Currency, DateValue, Id, Timestamp, @@ -19,7 +18,6 @@ describe('domain entity identity API conventions', () => { const transaction = Transaction.restore({ commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency: Currency.create('USD').valueOf(), description: 'Restored transaction', id: Id.create().valueOf(), isTombstone: false, diff --git a/apps/backend/src/domain/operations/operation.entity.test.ts b/apps/backend/src/domain/operations/operation.entity.test.ts index 351dcf7e..f8011e6f 100644 --- a/apps/backend/src/domain/operations/operation.entity.test.ts +++ b/apps/backend/src/domain/operations/operation.entity.test.ts @@ -1,5 +1,5 @@ import { apiErrorCodes } from '@ledgerly/shared/types'; -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { compareEntities, TransactionBuilder } from 'src/db/test-utils'; import { TransactionProps } from 'src/db/test-utils/testEntityBuilder'; import { @@ -31,7 +31,7 @@ describe('Operation Domain Entity', () => { let eurAccount: Account; const transactionData: TransactionProps = { - currencyCode: 'USD', + currency: 'USD', description: 'Test transaction', postingDate: '2024-01-01', transactionDate: '2024-01-01', @@ -48,11 +48,9 @@ describe('Operation Domain Entity', () => { beforeAll(async () => { user = await createUser(); userId = user.getId(); - const commodity = createCommodity(user, { code: 'USD' }); const data = TransactionBuilder.transaction({ - accounts: ['USD', 'EUR'], - commodity, + currencies: ['USD', 'EUR'], operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'EUR', amount: '-100', description: 'Credit' }, diff --git a/apps/backend/src/domain/transactions/transaction.entity.test.ts b/apps/backend/src/domain/transactions/transaction.entity.test.ts index 616dbfe0..ed33d286 100644 --- a/apps/backend/src/domain/transactions/transaction.entity.test.ts +++ b/apps/backend/src/domain/transactions/transaction.entity.test.ts @@ -1,5 +1,5 @@ import { apiErrorCodes } from '@ledgerly/shared/types'; -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { compareEntities, TransactionBuilder, @@ -32,7 +32,7 @@ import { import { Account } from '../accounts'; import { Commodity } from '../commodities'; -import { Amount, Currency, DateValue, Id, Version } from '../domain-core'; +import { Amount, DateValue, Id, Version } from '../domain-core'; import { CreateOperationProps, OperationSnapshot, @@ -93,7 +93,6 @@ const toCreateTransactionProps = ( >, ): CreateTransactionProps => ({ commodityId: fixture.transactionData.commodityId, - currency: Currency.create(fixture.transactionDTO.currencyCode), description: fixture.transactionData.description, operations: toCreateOperationProps( fixture.operationsData, @@ -124,7 +123,7 @@ describe('Transaction Domain Entity', () => { let transactionData: CreateTransactionProps; const transactionRawData: TransactionProps = { - currencyCode: 'USD', + currency: 'USD', description: 'Buy groceries', postingDate: '2024-01-01', transactionDate: '2024-01-01', @@ -194,16 +193,14 @@ describe('Transaction Domain Entity', () => { beforeAll(async () => { user = await createUser(); - commodity = createCommodity(user, { code: 'USD' }); - data = TransactionBuilder.transaction({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: operationsData, settings: transactionRawData, user, }); + commodity = data.transactionData.transactionCommodity; transactionData = toCreateTransactionProps(data); }); @@ -237,8 +234,6 @@ describe('Transaction Domain Entity', () => { expect(transaction.getVersion().valueOf()).toBe(0); expect(transaction.isDeleted()).toBe(false); - expect(transaction.currency).toBe(transactionData.currency); - const transactionOperations = transaction.getOperations(); expect(transactionOperations).toHaveLength(operationsData.length); @@ -1131,8 +1126,7 @@ describe('Transaction Domain Entity', () => { describe('Transaction Domain Invariants coverage', () => { it('should accept a multi-currency transaction balanced by value even when amounts do not sum to zero', () => { const data = TransactionBuilder.request({ - accounts: ['USD', 'RUB'], - commodity, + currencies: ['USD', 'RUB'], operations: [ { accountKey: 'USD', @@ -1158,8 +1152,7 @@ describe('Transaction Domain Entity', () => { it('should reject a multi-currency transaction with balanced amounts but unbalanced values', () => { const data = TransactionBuilder.request({ - accounts: ['USD', 'RUB'], - commodity, + currencies: ['USD', 'RUB'], operations: [ { accountKey: 'USD', @@ -1185,8 +1178,7 @@ describe('Transaction Domain Entity', () => { it('should validate a multi-currency operation patch by value rather than amount', () => { const data = TransactionBuilder.request({ - accounts: ['USD', 'RUB'], - commodity, + currencies: ['USD', 'RUB'], operations: [ { accountKey: 'USD', @@ -1247,8 +1239,7 @@ describe('Transaction Domain Entity', () => { ]; const data = TransactionBuilder.request({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: operationsData, settings: transactionRawData, user, @@ -1286,8 +1277,7 @@ describe('Transaction Domain Entity', () => { ]; const data = TransactionBuilder.request({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: operationsData, settings: transactionRawData, user, @@ -1332,8 +1322,7 @@ describe('Transaction Domain Entity', () => { it('should reject creation with zero operations', () => { const data = TransactionBuilder.request({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: [], settings: transactionRawData, user, @@ -1365,8 +1354,7 @@ describe('Transaction Domain Entity', () => { ]; const data = TransactionBuilder.request({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: operationsData, settings: transactionRawData, user, @@ -1423,8 +1411,7 @@ describe('Transaction Domain Entity', () => { ]; const data = TransactionBuilder.request({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: operationsData, settings: transactionRawData, user, @@ -1518,8 +1505,7 @@ describe('Transaction Domain Entity', () => { ]; const data = TransactionBuilder.request({ - accounts: ['USD', 'EUR', 'RUB', 'TRY'], - commodity, + currencies: ['USD', 'EUR', 'RUB', 'TRY'], operations: operationsData, settings: transactionRawData, user, diff --git a/apps/backend/src/domain/transactions/transaction.entity.ts b/apps/backend/src/domain/transactions/transaction.entity.ts index ee1fa00b..9c4e6fa6 100644 --- a/apps/backend/src/domain/transactions/transaction.entity.ts +++ b/apps/backend/src/domain/transactions/transaction.entity.ts @@ -21,7 +21,6 @@ import { ParentChildRelation, DateValue, Amount, - Currency, Version, } from '../domain-core'; import { Operation } from '../operations'; @@ -67,7 +66,6 @@ export class Transaction { private readonly ownership: ParentChildRelation, private postingDate: DateValue, private transactionDate: DateValue, - public currency: Currency, private readonly commodityId: ParentChildRelation, public description: string, @@ -96,7 +94,6 @@ export class Transaction { ownership, dto.postingDate, dto.transactionDate, - dto.currency, commodityRelation, dto.description, Version.create(0), @@ -162,7 +159,6 @@ export class Transaction { const { commodityId, createdAt, - currency, description, id, isTombstone, @@ -198,7 +194,6 @@ export class Transaction { ownership, DateValue.restore(postingDate), DateValue.restore(transactionDate), - Currency.restore(currency), commodityRelation, description, Version.restore(version), @@ -258,7 +253,6 @@ export class Transaction { return { commodityId: this.commodityId.getParentId().valueOf(), createdAt: this.getCreatedAt().valueOf(), - currency: this.currency.valueOf(), description: this.description, id: this.getId().valueOf(), isTombstone: this.isDeleted(), diff --git a/apps/backend/src/domain/transactions/types.ts b/apps/backend/src/domain/transactions/types.ts index a6966623..1eca6601 100644 --- a/apps/backend/src/domain/transactions/types.ts +++ b/apps/backend/src/domain/transactions/types.ts @@ -1,12 +1,7 @@ -import { - IsoDateString, - UUID, - CurrencyCode, - IsoDatetimeString, -} from '@ledgerly/shared/types'; +import { IsoDateString, UUID, IsoDatetimeString } from '@ledgerly/shared/types'; import { Account } from '../accounts'; -import { Currency, DateValue, Id } from '../domain-core'; +import { DateValue, Id } from '../domain-core'; import { CreateOperationProps, OperationProps, @@ -22,7 +17,6 @@ export type TransactionUpdateData = { export type TransactionBuildContext = { accountsMap: Map; - systemAccountsMap: Map; }; export type CreateTransactionProps = { @@ -30,7 +24,6 @@ export type CreateTransactionProps = { postingDate: DateValue; transactionDate: DateValue; operations: OperationProps[]; - currency: Currency; commodityId: UUID; }; @@ -57,7 +50,6 @@ export type TransactionSnapshot = { commodityId: UUID; operations: OperationSnapshot[]; version: number; - currency: CurrencyCode; }; export type TransactionSnapshotWithDetails = TransactionSnapshot & { diff --git a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts index 8273e429..6fae78ce 100644 --- a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.test.ts @@ -1,5 +1,5 @@ import { AccountDbRow } from 'src/db/schema'; -import { Amount, Currency, Id, Timestamp } from 'src/domain/domain-core'; +import { Amount, Id, Timestamp } from 'src/domain/domain-core'; import { describe, expect, it } from 'vitest'; import { AccountPersistenceMapper } from './account-persistence.mapper'; @@ -8,7 +8,6 @@ describe('AccountPersistenceMapper', () => { const row: AccountDbRow = { commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), - currency: Currency.create('USD').valueOf(), currentClearedBalanceLocal: Amount.create('2500').valueOf(), description: 'Operating account', id: Id.create().valueOf(), @@ -25,7 +24,6 @@ describe('AccountPersistenceMapper', () => { expect(AccountPersistenceMapper.toSnapshot(row)).toEqual({ commodityId: row.commodityId, createdAt: row.createdAt, - currency: row.currency, currentClearedBalanceLocal: row.currentClearedBalanceLocal, description: row.description, id: row.id, @@ -55,7 +53,6 @@ describe('AccountPersistenceMapper', () => { ).toEqual({ commodityId: row.commodityId, createdAt: row.createdAt, - currency: row.currency, currentClearedBalanceLocal: row.currentClearedBalanceLocal, description: row.description, id: row.id, diff --git a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts index ca6dd5a0..4d90127c 100644 --- a/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts +++ b/apps/backend/src/infrastructure/db/accounts/account-persistence.mapper.ts @@ -10,7 +10,6 @@ export class AccountPersistenceMapper { return { commodityId: row.commodityId, createdAt: row.createdAt, - currency: row.currency, currentClearedBalanceLocal: row.currentClearedBalanceLocal, description: row.description, id: row.id, @@ -28,7 +27,6 @@ export class AccountPersistenceMapper { return { commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, - currency: snapshot.currency, currentClearedBalanceLocal: snapshot.currentClearedBalanceLocal, description: snapshot.description, id: snapshot.id, diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts index e24de730..e520ae5c 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts @@ -1,9 +1,4 @@ -import { - apiErrorCodes, - AccountTypeValue, - CurrencyCode, - UUID, -} from '@ledgerly/shared/types'; +import { apiErrorCodes, AccountTypeValue, UUID } from '@ledgerly/shared/types'; import dayjs from 'dayjs'; import { AccountDbInsert, @@ -12,7 +7,6 @@ import { UserDbRow, } from 'src/db/schema'; import { Amount, CommodityCode, Timestamp } from 'src/domain/domain-core'; -import { Currency } from 'src/domain/domain-core/value-objects/Currency'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { AccountRepository } from 'src/infrastructure/db/'; import { @@ -27,18 +21,14 @@ import { TransactionManager } from '../TransactionManager'; const firstUserAccounts = ['firstUserAccount1', 'firstUserAccount2']; const secondUserAccounts = ['secondUserAccount1', 'secondUserAccount2']; -const USD: CurrencyCode = 'USD' as CurrencyCode; - const getAccountData = (params: { userId: UUID; commodityId: UUID; name: string; - currency: CurrencyCode; type: AccountTypeValue; }): AccountDbInsert => { return { commodityId: params.commodityId, - currency: params.currency, currentClearedBalanceLocal: Amount.create('0').valueOf(), description: 'This is a test account', initialBalance: Amount.create('100').valueOf(), @@ -54,7 +44,6 @@ const getAccountData = (params: { const accountDataRaw = { commodityId: 'non-existent-commodity-id' as UUID, - currency: USD, name: 'Test Account', type: 'asset' as const, userId: 'non-existent-user-id' as UUID, @@ -99,7 +88,6 @@ describe('AccountRepository', () => { it('should create a new account successfully', async () => { const newAccount = getAccountData({ commodityId: usdCommodity.id, - currency: USD, name: 'New Account', type: 'asset', userId: user.id, @@ -111,7 +99,6 @@ describe('AccountRepository', () => { expect(account).toHaveProperty('id'); expect(account.name).toBe(newAccount.name); - expect(account.currency).toBe(newAccount.currency); expect(account.type).toBe(newAccount.type); expect(account.userId).toBe(newAccount.userId); }); @@ -124,7 +111,6 @@ describe('AccountRepository', () => { const newAccount = getAccountData({ commodityId: usdCommodity.id, - currency: USD, name: 'New Account', type: 'asset', userId: secondUser.id, @@ -138,7 +124,6 @@ describe('AccountRepository', () => { it('should not allow duplicate account names for the same user', async () => { const newAccount = getAccountData({ commodityId: usdCommodity.id, - currency: USD, name: 'New Account', type: 'asset', userId: user.id, @@ -167,7 +152,6 @@ describe('AccountRepository', () => { const firstUserAccount = getAccountData({ commodityId: usdCommodity.id, - currency: USD, name: accountName, type: 'asset', userId: user.id, @@ -175,7 +159,6 @@ describe('AccountRepository', () => { const secondUserAccount = getAccountData({ commodityId: usdCommodity.id, - currency: USD, name: accountName, type: 'asset', userId: secondUser.id, @@ -218,7 +201,6 @@ describe('AccountRepository', () => { for (const name of firstUserAccounts) { await testDB.createAccount(user.id, usdCommodity.id, { - currency: USD, name, type: 'asset', }); @@ -226,7 +208,6 @@ describe('AccountRepository', () => { for (const name of secondUserAccounts) { await testDB.createAccount(secondUser.id, usdCommodity.id, { - currency: USD, name, type: 'asset', }); @@ -330,7 +311,6 @@ describe('AccountRepository', () => { const updatedAccountData = getAccountData({ commodityId: eurCommodity.id, - currency: 'EUR' as CurrencyCode, name: 'Updated Account', type: 'expense', userId: user2.id, @@ -345,7 +325,6 @@ describe('AccountRepository', () => { expect(updatedAccount).toBeDefined(); expect(updatedAccount?.id).toBe(account.id); expect(updatedAccount?.name).toBe(updatedAccountData.name); - expect(updatedAccount?.currency).toBe(updatedAccountData.currency); expect(updatedAccount?.type).toBe(updatedAccountData.type); expect(updatedAccount?.userId).toBe(user.id); }); @@ -353,7 +332,6 @@ describe('AccountRepository', () => { it('should return undefined when account belongs to different user', async () => { const updatedAccountData = getAccountData({ commodityId: eurCommodity.id, - currency: 'EUR' as CurrencyCode, name: 'Updated Account', type: 'expense', userId: user.id, @@ -373,14 +351,12 @@ describe('AccountRepository', () => { it('should not allow updating to duplicate name within same user', async () => { const updatedAccountData = getAccountData({ commodityId: usdCommodity.id, - currency: USD, name: 'Updated Account', type: 'asset', userId: user.id, }); await testDB.createAccount(user.id, usdCommodity.id, { - currency: USD, initialBalance: Amount.create('200').valueOf(), name: updatedAccountData.name, type: 'asset', @@ -409,7 +385,6 @@ describe('AccountRepository', () => { secondUser.id, usdCommodity.id, { - currency: USD, initialBalance: Amount.create('200').valueOf(), name: 'Shared Account Name', type: 'asset', @@ -420,7 +395,6 @@ describe('AccountRepository', () => { secondUser.id, secondUserAccount.id, { - currency: USD, name: accountData.name, type: 'asset', updatedAt: Timestamp.create().valueOf(), @@ -437,7 +411,6 @@ describe('AccountRepository', () => { it('should only update allowed fields', async () => { const maliciousData = { createdAt: Timestamp.create().valueOf(), - currency: Currency.create('EUR').valueOf(), id: 'malicious-id', initialBalance: Amount.create('2000').valueOf(), name: 'Updated Account', @@ -454,7 +427,6 @@ describe('AccountRepository', () => { expect(result?.id).toBe(account.id); expect(result?.userId).toBe(user.id); expect(result?.name).toBe(maliciousData.name); - expect(result?.currency).toBe(maliciousData.currency); }); }); @@ -500,7 +472,6 @@ describe('AccountRepository', () => { }); await testDB.createAccount(secondUser.id, usdCommodity.id, { - currency: USD, initialBalance: Amount.create('2000').valueOf(), name: 'Shared Account Name', type: 'asset', @@ -532,7 +503,6 @@ describe('AccountRepository', () => { const newAccount = getAccountData({ commodityId: usdCommodity.id, - currency: 'USD' as CurrencyCode, name: 'This is a test account', type: 'asset', userId: user.id, @@ -559,7 +529,6 @@ describe('AccountRepository', () => { const updatedData = getAccountData({ commodityId: eurCommodity.id, - currency: 'EUR' as CurrencyCode, name: 'Updated Account', type: 'expense', userId: user.id, @@ -578,41 +547,4 @@ describe('AccountRepository', () => { expect(originalUpdatedAt.unix()).toBeLessThanOrEqual(newUpdatedAt.unix()); }); }); - - describe('findSystemAccount', () => { - it('should retrieve system account by currency for a user', async () => { - const systemAccountData = getAccountData({ - commodityId: usdCommodity.id, - currency: USD, - name: 'System Account for USD', - type: 'asset', - userId: user.id, - }); - - const systemAccount = await testDB.createAccount( - user.id, - usdCommodity.id, - { - ...systemAccountData, - isSystem: true, - }, - ); - - const retrievedAccount = await accountRepository.findSystemAccount( - user.id, - USD, - ); - - expect(retrievedAccount).toBeDefined(); - expect(retrievedAccount.id).toBe(systemAccount.id); - expect(retrievedAccount.currency).toBe(USD); - expect(retrievedAccount.isSystem).toBe(true); - }); - - it('should throw RepositoryNotFoundError if system account does not exist for user and currency', async () => { - await expect( - accountRepository.findSystemAccount(user.id, USD), - ).rejects.toThrowError(RepositoryNotFoundError); - }); - }); }); diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.ts index 5e587c48..f1bb4b50 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.ts @@ -1,4 +1,4 @@ -import { CurrencyCode, UUID } from '@ledgerly/shared/types'; +import { UUID } from '@ledgerly/shared/types'; import { and, eq, inArray } from 'drizzle-orm'; import { type AccountRepositoryInterface, @@ -119,7 +119,6 @@ export class AccountRepository 'description', 'initialBalance', 'name', - 'currency', 'type', 'updatedAt', ]); @@ -173,34 +172,6 @@ export class AccountRepository }, `Failed to delete account with ID ${id}`); } - async findSystemAccount( - userId: UUID, - currency: CurrencyCode, - ): Promise { - return this.executeDatabaseOperation(async () => { - const account = await this.db - .select() - .from(accountsTable) - .where( - and( - eq(accountsTable.userId, userId), - eq(accountsTable.currency, currency), - eq(accountsTable.isSystem, true), - eq(accountsTable.isTombstone, false), - ), - ) - .get(); - - const existingAccount = this.ensureEntityExists( - account, - `System account not found for currency: ${currency}`, - this.entityNotFoundContext('account'), - ); - - return AccountPersistenceMapper.toSnapshot(existingAccount); - }, 'Failed to fetch system account'); - } - async ensureUserOwnsAccount(userId: UUID, accountId: UUID) { return this.executeDatabaseOperation(async () => { const account = await this.db diff --git a/apps/backend/src/infrastructure/db/currency/currency.repository.ts b/apps/backend/src/infrastructure/db/currency/currency.repository.ts deleted file mode 100644 index 1735e4d8..00000000 --- a/apps/backend/src/infrastructure/db/currency/currency.repository.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CurrencyResponseDTO } from 'src/application'; -import { currenciesTable } from 'src/db/schema'; - -import { BaseRepository } from '../BaseRepository'; - -export class CurrencyRepository extends BaseRepository { - getAll(): Promise { - return this.executeDatabaseOperation( - () => this.db.select().from(currenciesTable).all(), - 'Failed to fetch currencies', - ); - } -} diff --git a/apps/backend/src/infrastructure/db/currency/index.ts b/apps/backend/src/infrastructure/db/currency/index.ts deleted file mode 100644 index e3212852..00000000 --- a/apps/backend/src/infrastructure/db/currency/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CurrencyRepository } from './currency.repository'; diff --git a/apps/backend/src/infrastructure/db/index.ts b/apps/backend/src/infrastructure/db/index.ts index daf935ba..03babd8c 100644 --- a/apps/backend/src/infrastructure/db/index.ts +++ b/apps/backend/src/infrastructure/db/index.ts @@ -6,6 +6,5 @@ export { } from './transaction/'; export { OperationRepository } from './operations'; export { AccountRepository } from './accounts'; -export { CurrencyRepository } from './currency'; export { UserRepository } from './user'; export { CommodityRepository } from './commodities'; diff --git a/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts index 2029f64e..6128e08c 100644 --- a/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts +++ b/apps/backend/src/infrastructure/db/operations/operation-persistence.mapper.test.ts @@ -1,4 +1,4 @@ -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { User } from 'src/domain'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -13,11 +13,8 @@ describe('OperationPersistenceMapper', () => { }); it('maps an operation snapshot to a persistence row', () => { - const commodity = createCommodity(user, { code: 'USD' }); - const { operations } = TransactionBuilder.transaction({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'USD', amount: '-100', description: 'Credit' }, diff --git a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts index b411816b..1a4b55f2 100644 --- a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts +++ b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts @@ -44,10 +44,6 @@ describe('OperationRepository', () => { await testDB.setupTestDb(); userSnapshot = await testDB.createUser(); - const commodity = CommodityPersistenceMapper.toDomain( - await testDB.createCommodity(userSnapshot.id), - ); - const operationsData = [ { accountKey: 'USD', amount: '10000', description: '1' }, { accountKey: 'EUR', amount: '-10000', description: '2' }, @@ -58,8 +54,7 @@ describe('OperationRepository', () => { ]; data = TransactionBuilder.transaction({ - accounts: ['USD', 'EUR'], - commodity, + currencies: ['USD', 'EUR'], operations: operationsData, user: UserPersistenceMapper.toDomain(userSnapshot), }); diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts index 8e8320bb..0d39caa0 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.test.ts @@ -1,4 +1,4 @@ -import { createCommodity, createUser } from 'src/db/createTestUser'; +import { createUser } from 'src/db/createTestUser'; import { TransactionBuilder } from 'src/db/test-utils'; import { describe, expect, it } from 'vitest'; @@ -8,11 +8,8 @@ describe('TransactionPersistenceMapper', () => { it('maps a transaction aggregate to a persistence row', async () => { const user = await createUser(); - const commodity = createCommodity(user, { code: 'USD' }); - const { transaction } = TransactionBuilder.transaction({ - accounts: ['USD'], - commodity, + currencies: ['USD'], operations: [ { accountKey: 'USD', amount: '100', description: 'Debit' }, { accountKey: 'USD', amount: '-100', description: 'Credit' }, @@ -24,7 +21,6 @@ describe('TransactionPersistenceMapper', () => { expect(TransactionPersistenceMapper.toDBRow(transaction)).toEqual({ commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, - currency: snapshot.currency, description: snapshot.description, id: snapshot.id, isTombstone: snapshot.isTombstone, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts index 3e754bff..322b1c9d 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-persistence.mapper.ts @@ -8,7 +8,6 @@ export class TransactionPersistenceMapper { return { commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, - currency: snapshot.currency, description: snapshot.description, id: snapshot.id, isTombstone: snapshot.isTombstone, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts index 1362c261..5d7cfbbc 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts @@ -1,5 +1,5 @@ import { DEFAULT_TRANSACTION_QUERY } from '@ledgerly/shared/constants'; -import { CurrencyCode, IsoDateString, UUID } from '@ledgerly/shared/types'; +import { IsoDateString, UUID } from '@ledgerly/shared/types'; import { UserDbRow } from 'src/db/schema'; import { TestDB, TransactionSeed } from 'src/db/test-db'; import { CommodityCode, Name } from 'src/domain/domain-core'; @@ -9,7 +9,6 @@ import { TransactionManager, TransactionQueryRepository } from '../'; type TestAccount = { id: UUID; - currency: CurrencyCode; }; describe('TransactionQueryRepository', () => { @@ -37,7 +36,6 @@ describe('TransactionQueryRepository', () => { }); return { - currency: account.currency, id: account.id, }; }; @@ -55,7 +53,6 @@ describe('TransactionQueryRepository', () => { return; } - expect(transaction.currency).toBe(seed.currencyCode ?? 'USD'); expect(transaction.description).toBe(seed.description); expect(transaction.postingDate).toBe(seed.postingDate ?? '2023-01-01'); expect(transaction.transactionDate).toBe( @@ -151,7 +148,6 @@ describe('TransactionQueryRepository', () => { transactionDate: '2023-01-02' as IsoDateString, }, { - currencyCode: 'EUR' as CurrencyCode, description: 'Transfer', operations: [ { @@ -420,7 +416,6 @@ describe('TransactionQueryRepository', () => { transactionDate: '2023-01-02' as IsoDateString, }, { - currencyCode: 'EUR' as CurrencyCode, description: 'Transfer', operations: [ { @@ -491,7 +486,6 @@ describe('TransactionQueryRepository', () => { expect(Object.keys(transaction).sort()).toEqual( [ 'createdAt', - 'currency', 'description', 'id', 'operations', @@ -527,7 +521,6 @@ describe('TransactionQueryRepository', () => { describe('findById', () => { it('should return transaction by id with operations', async () => { const transactionSeed: TransactionSeed = { - currencyCode: 'EUR' as CurrencyCode, description: 'Exchange', operations: [ { diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts b/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts index 792c4464..4f6f2c0d 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts @@ -10,7 +10,6 @@ export class TransactionReadModelMapper { ): TransactionReadModel { return { createdAt: transaction.createdAt, - currency: transaction.currency, description: transaction.description, id: transaction.id, // mapOperation does not access this, so passing it unbound is safe. diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts index 971d5ffe..9815178c 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts @@ -6,7 +6,7 @@ import { TransactionBuilder, TransactionPersistenceBuilderResult, } from 'src/db/test-utils'; -import { Account, Commodity } from 'src/domain'; +import { Account } from 'src/domain'; import { Amount, DateValue, Id, Version } from 'src/domain/domain-core'; import { OperationSnapshot } from 'src/domain/operations/types'; import { RepositoryNotFoundError } from 'src/infrastructure/errors'; @@ -26,7 +26,6 @@ describe('TransactionRepository', () => { let testDB: TestDB; let transactionRepository: TransactionRepository; let user: UserDbRow; - let commodity: Commodity; let usdAccount: Account; let eurAccount: Account; let data: TransactionPersistenceBuilderResult; @@ -56,13 +55,8 @@ describe('TransactionRepository', () => { user = await testDB.createUser(); - commodity = CommodityPersistenceMapper.toDomain( - await testDB.createCommodity(user.id), - ); - data = TransactionBuilder.persistence({ - accounts: ['USD', 'EUR'], - commodity, + currencies: ['USD', 'EUR'], operations: [ { accountKey: 'USD', @@ -103,9 +97,6 @@ describe('TransactionRepository', () => { usdAccount = data.getAccountByKey('USD'); eurAccount = data.getAccountByKey('EUR'); - const usdSystemAccount = data.getSystemAccountByCurrency('USD'); - const eurSystemAccount = data.getSystemAccountByCurrency('EUR'); - await testDB.insertAccount( AccountPersistenceMapper.toDBRowFromSnapshot(usdAccount.toSnapshot()), ); @@ -114,18 +105,6 @@ describe('TransactionRepository', () => { AccountPersistenceMapper.toDBRowFromSnapshot(eurAccount.toSnapshot()), ); - await testDB.insertAccount( - AccountPersistenceMapper.toDBRowFromSnapshot( - usdSystemAccount.toSnapshot(), - ), - ); - - await testDB.insertAccount( - AccountPersistenceMapper.toDBRowFromSnapshot( - eurSystemAccount.toSnapshot(), - ), - ); - transactionRepository = new TransactionRepository( mockOperationsRepository as unknown as OperationRepository, transactionManager as unknown as TransactionManager, @@ -152,9 +131,8 @@ describe('TransactionRepository', () => { const insertedTransaction = await testDB.createTransactionWithOperations( user.id, - commodity.getId().valueOf(), + transaction.toSnapshot().commodityId, { - currencyCode: transaction.currency.valueOf(), description: transaction.description, operations: operationsDataToInsert, postingDate: transaction.getPostingDate().valueOf(), @@ -170,7 +148,7 @@ describe('TransactionRepository', () => { const retrievedTransactionSnapshot = retrievedTransaction?.toSnapshot(); expect(retrievedTransactionSnapshot?.commodityId).toEqual( - commodity.getId().valueOf(), + transaction.toSnapshot().commodityId, ); expect(retrievedTransaction).not.toBeNull(); @@ -713,8 +691,6 @@ describe('TransactionRepository', () => { deletedTransaction?.transactionDate, ); - expect(deletedTransaction?.currency).toBe(transaction.currency.valueOf()); - const expectedOperations: OperationDbRow[] = []; transaction.getAllOperations().forEach((operation) => { diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts index 16f2647a..d13dcd2b 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.ts @@ -101,7 +101,6 @@ export class TransactionRepository 'postingDate', 'transactionDate', 'updatedAt', - 'currency', 'version', ]); diff --git a/apps/backend/src/presentation/formatters/AmountFormatter.ts b/apps/backend/src/presentation/formatters/AmountFormatter.ts index 6779f283..f9ce15fe 100644 --- a/apps/backend/src/presentation/formatters/AmountFormatter.ts +++ b/apps/backend/src/presentation/formatters/AmountFormatter.ts @@ -38,42 +38,4 @@ export class AmountFormatter { return `${formattedMajor}${decimalSeparator}${minor}`; } - - formatWithCurrency( - amount: Amount, - currencyCode: string, - locale = 'en-US', - ): string { - const minorUnits = amount.valueOf(); - const { major, minor } = this.minorToMajor(minorUnits); - - const formatted = new Intl.NumberFormat(locale, { - currency: currencyCode, - style: 'currency', - }).format(Number(`${major}.${minor}`)); - - return formatted; - } - - formatCompact(amount: Amount, locale = 'en-US'): string { - const minorUnits = amount.valueOf(); - const { major, minor } = this.minorToMajor(minorUnits); - - return new Intl.NumberFormat(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 0, - notation: 'compact', - }).format(Number(`${major}.${minor}`)); - } - - formatForTable(amount: Amount, locale = 'en-US'): string { - const formatted = this.formatWithSign(amount, locale); - return formatted.padStart(15); - } - - formatWithSign(amount: Amount, locale = 'en-US'): string { - const formatted = this.format(amount, locale); - const sign = amount.isPositive() ? '+' : ''; - return `${sign}${formatted}`; - } } diff --git a/apps/backend/src/presentation/http/accounts/account.controller.test.ts b/apps/backend/src/presentation/http/accounts/account.controller.test.ts index c7d547b6..420ae229 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.test.ts @@ -7,7 +7,6 @@ import { UpdateAccountUseCase, } from 'src/application/usecases/accounts'; import { Amount } from 'src/domain/domain-core'; -import { Currency } from 'src/domain/domain-core/value-objects/Currency'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { User } from 'src/domain/users/user.entity'; import { createUser } from 'src/testing'; @@ -102,7 +101,6 @@ describe('AccountController', () => { it('should call accountService.create with correct data', async () => { const requestBody: AccountCreateDTO = { commodityId: Id.create().valueOf(), - currency: Currency.create('USD').valueOf(), description: 'Test Account', initialBalance: Amount.create('1000').valueOf(), name: 'New Account', @@ -139,7 +137,6 @@ describe('AccountController', () => { describe('update', () => { const requestBody: AccountUpdateDTO = { - currency: Currency.create('USD').valueOf(), description: 'Test Account', name: 'New Account', type: 'liability', diff --git a/apps/backend/src/presentation/http/accounts/account.integration.test.ts b/apps/backend/src/presentation/http/accounts/account.integration.test.ts index ec81c96e..2069b45b 100644 --- a/apps/backend/src/presentation/http/accounts/account.integration.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.integration.test.ts @@ -3,12 +3,10 @@ import { AccountCreateDTO, AccountResponseDTO, AccountTypeValue, - CurrencyCode, UUID, } from '@ledgerly/shared/types'; import { TestDB } from 'src/db/test-db'; import { Amount, CommodityCode } from 'src/domain/domain-core'; -import { Currency } from 'src/domain/domain-core/value-objects/Currency'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { createServer } from 'src/presentation/http'; import { describe, beforeEach, it, expect } from 'vitest'; @@ -18,7 +16,6 @@ const url = `/api${ROUTES.accounts}`; const firstUserAccounts = [ { commodityId: Id.create().valueOf(), - currency: Currency.create('USD').valueOf(), description: 'This is a test account', initialBalance: Amount.create('1000').valueOf(), name: 'Test Account', @@ -26,7 +23,6 @@ const firstUserAccounts = [ }, { commodityId: Id.create().valueOf(), - currency: Currency.create('USD').valueOf(), description: 'Savings account for future expenses', initialBalance: Amount.create('1000').valueOf(), name: 'Savings Account', @@ -130,7 +126,6 @@ describe('Accounts Integration Tests', () => { const payload: AccountCreateDTO = { commodityId: commodity.id, - currency: 'USD' as unknown as CurrencyCode, description: 'This is a new account', initialBalance: Amount.create('1000').valueOf(), name: 'New Account', @@ -150,7 +145,6 @@ describe('Accounts Integration Tests', () => { expect(response.statusCode).toBe(201); expect(createdAccount.name).toBe(payload.name); - expect(createdAccount.currency).toBe(payload.currency); expect(createdAccount.type).toBe(payload.type); expect(createdAccount.description).toBe(payload.description); @@ -224,7 +218,6 @@ describe('Accounts Integration Tests', () => { expect(response.statusCode).toBe(200); expect(updatedAccount.name).toBe(updatedData.name); - expect(updatedAccount.currency).toBe(updatedData.currency); const finalResponse = await server.inject({ headers: { diff --git a/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts b/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts index ebe13f56..75463b55 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.controller.test.ts @@ -13,7 +13,7 @@ import { GetAllTransactionsUseCase } from 'src/application/usecases/transaction/ import { GetTransactionByIdUseCase } from 'src/application/usecases/transaction/GetTransactionById'; import { UpdateTransactionUseCase } from 'src/application/usecases/transaction/UpdateTransaction'; import { User } from 'src/domain'; -import { Amount, Currency, DateValue, Id } from 'src/domain/domain-core'; +import { Amount, DateValue, Id } from 'src/domain/domain-core'; import { createUser } from 'src/testing'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { ZodError } from 'zod'; @@ -93,7 +93,6 @@ describe('TransactionController', () => { it('should call CreateTransactionUseCase with correct parameters', async () => { const requestBody: TransactionCreateInput = { commodityId: Id.create().valueOf(), - currencyCode: Currency.create('USD').valueOf(), description: 'Test Transaction', operations, postingDate: DateValue.restore('2024-01-01').valueOf(), @@ -105,7 +104,6 @@ describe('TransactionController', () => { expect(mockCreateTransactionUseCase.execute).toHaveBeenCalledWith( user, expect.objectContaining({ - currencyCode: requestBody.currencyCode, description: requestBody.description, operations: requestBody.operations, postingDate: requestBody.postingDate, @@ -120,7 +118,6 @@ describe('TransactionController', () => { it('should throw ZodError for invalid request body', async () => { const invalidRequestBody = { - currencyCode: 123, description: 'Test Transaction', operations: [], postingDate: 'invalid-date', @@ -139,7 +136,6 @@ describe('TransactionController', () => { 'should reject a create request without %s', async (missingDateField) => { const requestBody = { - currencyCode: Currency.create('USD').valueOf(), description: 'Test Transaction', operations, postingDate: DateValue.restore('2024-01-01').valueOf(), diff --git a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts index a8a600c2..38393a14 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts @@ -24,7 +24,7 @@ import { import { CreateTransactionProps, TestDB } from 'src/db/test-db'; import { compareEntities } from 'src/db/test-utils'; import { compareCommonEntities } from 'src/db/test-utils/entityComparer'; -import { Amount, Currency, DateValue, Id } from 'src/domain/domain-core'; +import { Amount, DateValue, Id } from 'src/domain/domain-core'; import { createServer } from 'src/presentation/http'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -79,11 +79,9 @@ describe('Transactions Integration Tests', () => { const createAccounts = async () => { return Promise.all([ testDB.createAccount(userId, commodityId, { - currency: Currency.create('USD').valueOf(), name: 'Checking USD', }), testDB.createAccount(userId, commodityId, { - currency: Currency.create('USD').valueOf(), name: 'Savings USD', }), ]); @@ -116,7 +114,6 @@ describe('Transactions Integration Tests', () => { it('should create a new transaction', async () => { const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'some transaction', operations: [operation1, operation2], postingDate: DateValue.restore('2025-11-07').valueOf(), @@ -154,13 +151,11 @@ describe('Transactions Integration Tests', () => { it('should create a multi-currency transaction balanced by value when amounts do not sum to zero', async () => { const rubAccount = await testDB.createAccount(userId, commodityId, { - currency: Currency.create('RUB').valueOf(), name: 'Cash RUB', }); const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'USD to RUB exchange', operations: [ { @@ -222,7 +217,7 @@ describe('Transactions Integration Tests', () => { expect.arrayContaining([ { code: 'REQUIRED', - path: 'currencyCode', + path: 'commodityId', }, { code: 'REQUIRED', @@ -235,7 +230,6 @@ describe('Transactions Integration Tests', () => { it('should fail with invalid amounts', async () => { const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'invalid amount', operations: [ operation1, @@ -278,7 +272,6 @@ describe('Transactions Integration Tests', () => { ])('should fail with %s', async (_, invalidOperationPatch) => { const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'invalid finite amount', operations: [ operation1, @@ -309,7 +302,6 @@ describe('Transactions Integration Tests', () => { it('should fail for unauthorized access', async () => { const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'unauthorized access', operations: [operation1, operation2], postingDate: DateValue.restore('2025-11-07').valueOf(), @@ -328,7 +320,6 @@ describe('Transactions Integration Tests', () => { it('should fail for non-existent accounts', async () => { const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'non-existent account', operations: [ { ...operation1, accountId: Id.create().valueOf() }, @@ -353,7 +344,6 @@ describe('Transactions Integration Tests', () => { it('should fail when operations do not sum to zero (unbalanced transaction)', async () => { const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'unbalanced transaction', operations: [ operation1, @@ -389,14 +379,12 @@ describe('Transactions Integration Tests', () => { otherUser.id, commodityId, { - currency: Currency.create('USD').valueOf(), name: 'Other User Account', }, ); const payload: TransactionCreateInput = { commodityId, - currencyCode: Currency.create('USD').valueOf(), description: 'unauthorized access', operations: [ { ...operation1, accountId: otherUserAccount.id }, @@ -664,7 +652,6 @@ describe('Transactions Integration Tests', () => { it('should retrieve all transactions for the user', async () => { const transaction1Params: CreateTransactionProps = { - currencyCode: accounts[0].currency, description: 'transaction one', isTombstone: false, postingDate: DateValue.create().valueOf(), @@ -672,7 +659,6 @@ describe('Transactions Integration Tests', () => { }; const transaction2Params: CreateTransactionProps = { - currencyCode: accounts[1].currency, description: 'transaction two', isTombstone: false, postingDate: DateValue.create().valueOf(), @@ -1817,7 +1803,6 @@ describe('Transactions Integration Tests', () => { otherUser.id, commodityId, { - currency: Currency.create('USD').valueOf(), name: 'Other User Account', }, ); @@ -1904,7 +1889,6 @@ describe('Transactions Integration Tests', () => { otherUser.id, commodityId, { - currency: Currency.create('USD').valueOf(), name: 'Other User Account', }, ); diff --git a/packages/shared/src/types/account.ts b/packages/shared/src/types/account.ts index 74d12d8d..b7ce1c5d 100644 --- a/packages/shared/src/types/account.ts +++ b/packages/shared/src/types/account.ts @@ -1,4 +1,4 @@ -import { CurrencyCode, IsoDatetimeString, AmountString, UUID } from "./types"; +import { IsoDatetimeString, AmountString, UUID } from "./types"; export type AccountTypeValue = | "asset" @@ -11,7 +11,6 @@ export type AccountTypeValue = export type AccountDomain = { commodityId: UUID; createdAt: IsoDatetimeString; - currency: CurrencyCode; currentClearedBalanceLocal: AmountString; description: string; id: UUID; @@ -26,7 +25,6 @@ export type AccountDomain = { export type AccountCreateDTO = { commodityId: UUID; - currency: CurrencyCode; description: string; initialBalance: AmountString; isSystem?: boolean; @@ -35,10 +33,7 @@ export type AccountCreateDTO = { }; export type AccountUpdateDTO = Partial< - Pick< - AccountCreateDTO, - "name" | "description" | "type" | "currency" | "isSystem" - > + Pick >; export type AccountResponseDTO = AccountDomain; diff --git a/packages/shared/src/types/transaction.ts b/packages/shared/src/types/transaction.ts index c0cbc707..a0c26fc9 100644 --- a/packages/shared/src/types/transaction.ts +++ b/packages/shared/src/types/transaction.ts @@ -4,7 +4,6 @@ import { getTransactionsQuerySchema } from "../validation/baseValidations"; import type { PaginatedResponse } from "./pagination"; import type { - CurrencyCode, IsoDateString, IsoDatetimeString, AmountString, @@ -44,7 +43,6 @@ export type OperationResponseDTO = { export type CreateTransactionRequestDTO = { commodityId: UUID; - currencyCode: CurrencyCode; description: string; operations: CreateOperationRequestDTO[]; postingDate: IsoDateString; @@ -65,7 +63,6 @@ export type UpdateTransactionRequestDTO = { export type TransactionResponseDTO = { createdAt: IsoDatetimeString; - currency: CurrencyCode; description: string; id: UUID; operations: OperationResponseDTO[]; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 0408bdad..686aab6b 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { - currencyCode, isoDate, isoDatetime, sha256String, @@ -12,8 +11,6 @@ import { commoditySymbol, } from "../validation/baseValidations"; -export type CurrencyCode = z.infer; - export type IsoDatetimeString = z.infer; export type IsoDateString = z.infer; export type Sha256String = z.infer; diff --git a/packages/shared/src/validation/accounts.ts b/packages/shared/src/validation/accounts.ts index 9c8d946e..d8ffa988 100644 --- a/packages/shared/src/validation/accounts.ts +++ b/packages/shared/src/validation/accounts.ts @@ -5,7 +5,6 @@ import { ACCOUNT_TYPE_VALUES } from "../constants"; import { notNullText, requiredText, - currencyCode, isoDatetime, uuid, amountString, @@ -15,7 +14,6 @@ const accountType = z.enum(ACCOUNT_TYPE_VALUES); export const accountCreateSchema = z.object({ commodityId: uuid, - currency: currencyCode, description: requiredText, initialBalance: amountString, name: notNullText, @@ -24,7 +22,6 @@ export const accountCreateSchema = z.object({ export const accountUpdateSchema = accountCreateSchema .pick({ - currency: true, description: true, name: true, type: true, @@ -33,7 +30,6 @@ export const accountUpdateSchema = accountCreateSchema export const accountResponseSchema = z.object({ createdAt: isoDatetime, - currency: currencyCode, currentClearedBalanceLocal: z.number(), description: requiredText, id: uuid, diff --git a/packages/shared/src/validation/baseValidations.ts b/packages/shared/src/validation/baseValidations.ts index 62fc00cf..7262cd59 100644 --- a/packages/shared/src/validation/baseValidations.ts +++ b/packages/shared/src/validation/baseValidations.ts @@ -27,11 +27,6 @@ export const uniqueIdSchema = z.object({ id: uuid, }); -export const currencyCode = z - .string() - .length(3, "Currency code must be exactly 3 characters") - .brand<"CurrencyCode">(); - export const isoDatetime = z.string().datetime().brand<"IsoDatetimeString">(); export const sha256String = z.string().regex(/^[a-f0-9]{64}$/, { diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index ccf62b23..cc0aa339 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -11,7 +11,6 @@ import { isoDate, isoDatetime, amountString, - currencyCode, } from "./baseValidations"; // amount — posting in the account's native currency @@ -36,7 +35,6 @@ export const operationUpdateSchema = z.object({ export const transactionCreateSchema = z.object({ commodityId: uuid, - currencyCode: currencyCode, description: requiredText, operations: z .array(operationCreateSchema) From acb7bd029225a0763b6a9c4aebcb3cb92888c6a1 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 09:20:26 +0300 Subject: [PATCH 10/30] LED-66 feat(database): implement commodity schema migration and remove currency references --- ...onster.sql => 0000_serious_ser_duncan.sql} | 10 - .../backend/drizzle/0001_overrated_marrow.sql | 15 - apps/backend/drizzle/meta/0000_snapshot.json | 68 +- apps/backend/drizzle/meta/0001_snapshot.json | 614 ------------------ apps/backend/drizzle/meta/_journal.json | 11 +- apps/backend/package.json | 1 - .../http/currencies/currencies.routes.ts | 7 - .../src/presentation/http/currencies/index.ts | 1 - apps/backend/src/presentation/http/index.ts | 1 - .../src/presentation/http/register-routes.ts | 5 - packages/shared/src/routes/index.ts | 1 - 11 files changed, 3 insertions(+), 731 deletions(-) rename apps/backend/drizzle/{0000_nosy_professor_monster.sql => 0000_serious_ser_duncan.sql} (90%) delete mode 100644 apps/backend/drizzle/0001_overrated_marrow.sql delete mode 100644 apps/backend/drizzle/meta/0001_snapshot.json delete mode 100644 apps/backend/src/presentation/http/currencies/currencies.routes.ts delete mode 100644 apps/backend/src/presentation/http/currencies/index.ts diff --git a/apps/backend/drizzle/0000_nosy_professor_monster.sql b/apps/backend/drizzle/0000_serious_ser_duncan.sql similarity index 90% rename from apps/backend/drizzle/0000_nosy_professor_monster.sql rename to apps/backend/drizzle/0000_serious_ser_duncan.sql index 08394aed..f1713a88 100644 --- a/apps/backend/drizzle/0000_nosy_professor_monster.sql +++ b/apps/backend/drizzle/0000_serious_ser_duncan.sql @@ -1,7 +1,6 @@ CREATE TABLE `accounts` ( `commodity_id` text NOT NULL, `created_at` text NOT NULL, - `currency` text NOT NULL, `current_cleared_balance_local` text NOT NULL, `description` text NOT NULL, `id` text PRIMARY KEY NOT NULL, @@ -20,7 +19,6 @@ CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`);- CREATE TABLE `transactions` ( `valuation_commodity_id` text NOT NULL, `created_at` text NOT NULL, - `currency` text NOT NULL, `description` text NOT NULL, `id` text PRIMARY KEY NOT NULL, `is_tombstone` integer NOT NULL, @@ -34,12 +32,6 @@ CREATE TABLE `transactions` ( ); --> statement-breakpoint CREATE INDEX `idx_transactions_user_date` ON `transactions` (`user_id`,`transaction_date`);--> statement-breakpoint -CREATE TABLE `currencies` ( - `code` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `symbol` text NOT NULL -); ---> statement-breakpoint CREATE TABLE `operations` ( `account_id` text NOT NULL, `amount` text NOT NULL, @@ -71,11 +63,9 @@ CREATE TABLE `users` ( --> statement-breakpoint CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint CREATE TABLE `settings` ( - `base_currency` text DEFAULT 'RUB' NOT NULL, `created_at` text NOT NULL, `updated_at` text NOT NULL, `user_id` text NOT NULL, - FOREIGN KEY (`base_currency`) REFERENCES `currencies`(`code`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint diff --git a/apps/backend/drizzle/0001_overrated_marrow.sql b/apps/backend/drizzle/0001_overrated_marrow.sql deleted file mode 100644 index 7bf70945..00000000 --- a/apps/backend/drizzle/0001_overrated_marrow.sql +++ /dev/null @@ -1,15 +0,0 @@ -DROP TABLE `currencies`;--> statement-breakpoint -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_settings` ( - `created_at` text NOT NULL, - `updated_at` text NOT NULL, - `user_id` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_settings`("created_at", "updated_at", "user_id") SELECT "created_at", "updated_at", "user_id" FROM `settings`;--> statement-breakpoint -DROP TABLE `settings`;--> statement-breakpoint -ALTER TABLE `__new_settings` RENAME TO `settings`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -ALTER TABLE `accounts` DROP COLUMN `currency`;--> statement-breakpoint -ALTER TABLE `transactions` DROP COLUMN `currency`; \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/meta/0000_snapshot.json index 023c5227..610b6821 100644 --- a/apps/backend/drizzle/meta/0000_snapshot.json +++ b/apps/backend/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "67f3394c-dc2e-4f8d-95c2-f74c1aef120c", + "id": "e16bd8ef-8add-4e77-bb28-3325a74ab237", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "accounts": { @@ -21,13 +21,6 @@ "notNull": true, "autoincrement": false }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "current_cleared_balance_local": { "name": "current_cleared_balance_local", "type": "text", @@ -158,13 +151,6 @@ "notNull": true, "autoincrement": false }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "description": { "name": "description", "type": "text", @@ -264,37 +250,6 @@ "uniqueConstraints": {}, "checkConstraints": {} }, - "currencies": { - "name": "currencies", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, "operations": { "name": "operations", "columns": { @@ -507,14 +462,6 @@ "settings": { "name": "settings", "columns": { - "base_currency": { - "name": "base_currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'RUB'" - }, "created_at": { "name": "created_at", "type": "text", @@ -539,19 +486,6 @@ }, "indexes": {}, "foreignKeys": { - "settings_base_currency_currencies_code_fk": { - "name": "settings_base_currency_currencies_code_fk", - "tableFrom": "settings", - "tableTo": "currencies", - "columnsFrom": [ - "base_currency" - ], - "columnsTo": [ - "code" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, "settings_user_id_users_id_fk": { "name": "settings_user_id_users_id_fk", "tableFrom": "settings", diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/meta/0001_snapshot.json deleted file mode 100644 index c8fc7f1c..00000000 --- a/apps/backend/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,614 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "f9ae6d92-c304-4529-a9ee-11183f004cff", - "prevId": "67f3394c-dc2e-4f8d-95c2-f74c1aef120c", - "tables": { - "accounts": { - "name": "accounts", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "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", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "initial_balance": { - "name": "initial_balance", - "type": "text", - "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", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_id_name_unique_idx": { - "name": "user_id_name_unique_idx", - "columns": [ - "user_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "accounts_commodity_id_commodities_id_fk": { - "name": "accounts_commodity_id_commodities_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "transactions": { - "name": "transactions", - "columns": { - "valuation_commodity_id": { - "name": "valuation_commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "posting_date": { - "name": "posting_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_date": { - "name": "transaction_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_transactions_user_date": { - "name": "idx_transactions_user_date", - "columns": [ - "user_id", - "transaction_date" - ], - "isUnique": false - } - }, - "foreignKeys": { - "transactions_valuation_commodity_id_commodities_id_fk": { - "name": "transactions_valuation_commodity_id_commodities_id_fk", - "tableFrom": "transactions", - "tableTo": "commodities", - "columnsFrom": [ - "valuation_commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", - "tableFrom": "transactions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "operations": { - "name": "operations", - "columns": { - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "amount": { - "name": "amount", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_id": { - "name": "transaction_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_operations_transaction": { - "name": "idx_operations_transaction", - "columns": [ - "transaction_id" - ], - "isUnique": false - }, - "idx_operations_account": { - "name": "idx_operations_account", - "columns": [ - "account_id" - ], - "isUnique": false - }, - "idx_operations_user": { - "name": "idx_operations_user", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", - "tableFrom": "operations", - "tableTo": "accounts", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", - "tableFrom": "operations", - "tableTo": "transactions", - "columnsFrom": [ - "transaction_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", - "tableFrom": "operations", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "settings": { - "name": "settings", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "settings_user_id_users_id_fk": { - "name": "settings_user_id_users_id_fk", - "tableFrom": "settings", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "commodities": { - "name": "commodities", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "precision": { - "name": "precision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "commodities_user_id_code_unique_idx": { - "name": "commodities_user_id_code_unique_idx", - "columns": [ - "user_id", - "code" - ], - "isUnique": true - } - }, - "foreignKeys": { - "commodities_user_id_users_id_fk": { - "name": "commodities_user_id_users_id_fk", - "tableFrom": "commodities", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index 23360456..988a0484 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "6", - "when": 1784739467705, - "tag": "0000_nosy_professor_monster", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1784743725510, - "tag": "0001_overrated_marrow", + "when": 1784787321727, + "tag": "0000_serious_ser_duncan", "breakpoints": true } ] diff --git a/apps/backend/package.json b/apps/backend/package.json index a0c3484d..82486d9c 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -28,7 +28,6 @@ "db:delete:all": "pnpm db:delete && pnpm db:migrations:delete", "db:full-reset": "pnpm db:delete:all && pnpm db:generate && pnpm db:migrate", "testDB": "tsx src/db/test-db.ts", - "seed:currencies": "tsx src/db/scripts/currenciesSeed.ts", "db:reset": "pnpm db:delete && pnpm db:migrate", "lint": "eslint .", "lint:fix": "eslint . --fix", diff --git a/apps/backend/src/presentation/http/currencies/currencies.routes.ts b/apps/backend/src/presentation/http/currencies/currencies.routes.ts deleted file mode 100644 index 535631e4..00000000 --- a/apps/backend/src/presentation/http/currencies/currencies.routes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { FastifyInstance } from 'fastify'; - -export const registerCurrenciesRoutes = (app: FastifyInstance) => { - app.get('/', () => { - throw new Error('Not implemented'); - }); -}; diff --git a/apps/backend/src/presentation/http/currencies/index.ts b/apps/backend/src/presentation/http/currencies/index.ts deleted file mode 100644 index b8f9d4a6..00000000 --- a/apps/backend/src/presentation/http/currencies/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { registerCurrenciesRoutes } from './currencies.routes'; diff --git a/apps/backend/src/presentation/http/index.ts b/apps/backend/src/presentation/http/index.ts index 490a0c48..000a78ee 100644 --- a/apps/backend/src/presentation/http/index.ts +++ b/apps/backend/src/presentation/http/index.ts @@ -1,6 +1,5 @@ export { AccountController, accountsRoutes } from './accounts'; export { AuthController, authRoutes } from './auth'; -export { registerCurrenciesRoutes } from './currencies'; export { errorHandler, getValidationFieldErrorCode } from './error-handler'; export { HttpApiError, UnauthorizedError } from './errors'; export { registerRoutes } from './register-routes'; diff --git a/apps/backend/src/presentation/http/register-routes.ts b/apps/backend/src/presentation/http/register-routes.ts index 8c466abf..ed0efb05 100644 --- a/apps/backend/src/presentation/http/register-routes.ts +++ b/apps/backend/src/presentation/http/register-routes.ts @@ -3,7 +3,6 @@ import type { FastifyInstance } from 'fastify'; import { accountsRoutes } from './accounts'; import { authRoutes } from './auth'; -import { registerCurrenciesRoutes } from './currencies'; import { authMiddleware } from './middleware'; import { transactionsRoutes } from './transactions'; import { registerUserRoutes } from './users'; @@ -16,10 +15,6 @@ export const registerRoutes = (fastify: FastifyInstance) => { fastify.register(authRoutes, { prefix: ROUTES.auth }); - fastify.register(registerCurrenciesRoutes, { - prefix: ROUTES.currencies, - }); - // Protected routes fastify.register((protectedApp) => { protectedApp.addHook('onRequest', authMiddleware); diff --git a/packages/shared/src/routes/index.ts b/packages/shared/src/routes/index.ts index 74bb0390..9a90c9bd 100644 --- a/packages/shared/src/routes/index.ts +++ b/packages/shared/src/routes/index.ts @@ -1,7 +1,6 @@ export const ROUTES = { accounts: "/accounts", auth: "/auth", - currencies: "/currencies", entries: "/entries", transactions: "/transactions", user: "/user", From e87ef38f62783f5f88b293ad84586434a51777c1 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 09:27:01 +0300 Subject: [PATCH 11/30] LED-66 docs(database): update database schema documentation for commodity model and remove currency references --- docs/DATABASE_SCHEMA.md | 310 ++++++++++-------- docs/DOMAIN.md | 42 ++- docs/MULTICURRENCY_DESIGN.md | 24 +- ...ity-registry-before-currency-validation.md | 13 +- 4 files changed, 213 insertions(+), 176 deletions(-) diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 412277a7..287b1be7 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -1,17 +1,22 @@ -# 📊 Database Schema - Ledgerly +# Database Schema - Ledgerly Database schema documentation for Ledgerly financial tracker. -## 🎯 Overview +## Overview -Ledgerly uses SQLite with Drizzle ORM for personal finance management. The architecture is built on these principles: -- **User data isolation** - all data is tied to userId -- **Double-entry bookkeeping** - operations belong directly to transactions -- **Multi-currency support** - different currencies with trading accounts -- **Cascade deletions** - data integrity -- **Soft deletes** - using `isTombstone` flag +Ledgerly uses SQLite with Drizzle ORM for personal finance management. The +architecture is built on these principles: -## 📋 Entity Relationship Diagram +- **User data isolation** - all user-owned data is tied to `userId`. +- **Double-entry bookkeeping** - operations belong directly to transactions. +- **Commodity-based accounting** - accounts and transactions reference + user-owned Commodities by stable id. +- **Cascade deletions** - user-owned data is removed with the user where the + schema defines cascading relations. +- **Soft deletes** - domain records use an `isTombstone` flag where deletion + must remain observable. + +## Entity Relationship Diagram ```mermaid erDiagram @@ -23,18 +28,24 @@ erDiagram timestamp createdAt timestamp updatedAt } - - CURRENCIES { - string code PK "USD, EUR, RUB" + + COMMODITIES { + uuid id PK + uuid userId FK + string code string name - string symbol + string symbol "nullable" + integer precision + boolean isTombstone + timestamp createdAt + timestamp updatedAt } - + ACCOUNTS { uuid id PK + uuid commodityId FK string name string type "Asset|Liability|Income|Expense" - string currency FK string description integer initialBalance integer currentClearedBalanceLocal @@ -44,11 +55,11 @@ erDiagram timestamp createdAt timestamp updatedAt } - + TRANSACTIONS { uuid id PK + uuid valuationCommodityId FK string description - string currency date transactionDate date postingDate integer version @@ -57,7 +68,7 @@ erDiagram timestamp createdAt timestamp updatedAt } - + OPERATIONS { uuid id PK uuid transactionId FK @@ -71,33 +82,33 @@ erDiagram timestamp createdAt timestamp updatedAt } - + SETTINGS { - uuid userId PK,FK - string baseCurrency FK + uuid userId FK timestamp createdAt timestamp updatedAt } - %% Main relations + USERS ||--o{ COMMODITIES : "owns" USERS ||--o{ ACCOUNTS : "owns" USERS ||--o{ TRANSACTIONS : "makes" USERS ||--o{ OPERATIONS : "performs" - USERS ||--|| SETTINGS : "has" - - CURRENCIES ||--o{ ACCOUNTS : "denominated_in" - CURRENCIES ||--o{ SETTINGS : "base_currency" - + USERS ||--o{ SETTINGS : "has" + + COMMODITIES ||--o{ ACCOUNTS : "denominates" + COMMODITIES ||--o{ TRANSACTIONS : "values" + TRANSACTIONS ||--o{ OPERATIONS : "contains" ACCOUNTS ||--o{ OPERATIONS : "affects" - - %% Unique constraints + + COMMODITIES }|--|| USERS : "unique(userId, code)" ACCOUNTS }|--|| USERS : "unique(userId, name)" ``` -## 🏗️ Entities +## Entities + +### Users -### 🏦 **Users** Base entity for authentication and authorization. | Field | Type | Description | Constraints | @@ -110,215 +121,246 @@ Base entity for authentication and authorization. | `updatedAt` | Timestamp | Last update date | NOT NULL | **Relations:** + +- `1:N` with `commodities` (cascade delete) - `1:N` with `accounts` (cascade delete) - `1:N` with `transactions` (cascade delete) - `1:N` with `operations` (cascade delete) -- `1:1` with `settings` +- `1:N` with `settings` --- -### 💰 **Accounts** -User's financial accounts for tracking funds. +### Commodities + +User-owned monetary units used by accounts and transactions. | Field | Type | Description | Constraints | |------|-----|----------|-------------| -| `id` | UUID | Primary key | PK, NOT NULL | -| `name` | String | Account name | NOT NULL | -| `type` | Enum | Account type | `Asset\|Liability\|Income\|Expense` | -| `currency` | String | Account currency | NOT NULL (CurrencyCode) | -| `description` | String | Account description | NULLABLE | -| `initialBalance` | Integer | Initial balance (in cents) | NOT NULL, default: 0 | -| `currentClearedBalanceLocal` | Integer | Current balance (in cents) | NOT NULL, default: 0 | -| `isSystem` | Boolean | System account flag (for trading accounts) | NOT NULL, default: false | -| `isTombstone` | Boolean | Soft delete flag | NOT NULL, default: false | -| `userId` | UUID | Account owner | FK → `users.id` | +| `id` | UUID | Stable Commodity identity | PK, NOT NULL | +| `userId` | UUID | Commodity owner | FK -> `users.id`, NOT NULL, ON DELETE CASCADE | +| `code` | String | User-visible display/search code | NOT NULL, unique per user | +| `name` | String | Display name | NOT NULL | +| `symbol` | String | Optional display symbol | NULLABLE | +| `precision` | Integer | Minor-unit scale for integer amounts | NOT NULL, immutable after creation | +| `isTombstone` | Boolean | Technical soft delete flag | NOT NULL, default: false | | `createdAt` | Timestamp | Creation date | NOT NULL | | `updatedAt` | Timestamp | Last update date | NOT NULL | **Constraints:** -- `UNIQUE(userId, name)` - unique name per user + +- `UNIQUE(userId, code)` through `commodities_user_id_code_unique_idx`. **Relations:** + - `N:1` with `users` -- `1:N` with `operations` +- `1:N` with `accounts` +- `1:N` with `transactions` **Notes:** -- Currency constraint to `currencies` table is removed for test performance -- Application-level validation should ensure valid currency codes -- Money amounts stored as integers (cents) to avoid floating-point issues -- System accounts (trading accounts) have `isSystem = true` + +- Commodity identity is `id`, not `code`. +- Predefined currencies such as `USD`, `EUR`, and `RUB` are ordinary per-user + Commodity records when created. +- The MVP does not include global Commodity records, `ReferenceAsset`, market + rates, provider metadata, automatic conversion, or Commodity archiving. --- -### 💱 **Currencies** -Legacy dictionary of supported currencies. +### Accounts -> Design note: this table is not the final identity model for monetary units. -> The target model is a user-owned `commodities` table. Commodity identity is a -> stable id, not `code`. See -> [ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). +User's financial accounts for tracking funds. | Field | Type | Description | Constraints | |------|-----|----------|-------------| -| `code` | String | Currency code (ISO 4217) | PK, NOT NULL | -| `name` | String | Full name | NOT NULL | -| `symbol` | String | Currency symbol | NOT NULL | +| `id` | UUID | Primary key | PK, NOT NULL | +| `commodityId` | UUID | Account Commodity | FK -> `commodities.id`, NOT NULL | +| `name` | String | Account name | NOT NULL | +| `type` | Enum | Account type | `Asset\|Liability\|Income\|Expense` | +| `description` | String | Account description | NOT NULL | +| `initialBalance` | Integer | Initial balance in minor units | NOT NULL, default: 0 | +| `currentClearedBalanceLocal` | Integer | Current local balance in minor units | NOT NULL, default: 0 | +| `isSystem` | Boolean | Reserved system account flag | NOT NULL, default: false | +| `isTombstone` | Boolean | Soft delete flag | NOT NULL, default: false | +| `userId` | UUID | Account owner | FK -> `users.id`, NOT NULL, ON DELETE CASCADE | +| `createdAt` | Timestamp | Creation date | NOT NULL | +| `updatedAt` | Timestamp | Last update date | NOT NULL | -**Examples:** -- `USD` - United States Dollar - `$` -- `EUR` - Euro - `€` -- `RUB` - Russian Ruble - `₽` +**Constraints:** + +- `UNIQUE(userId, name)` through `user_id_name_unique_idx`. **Relations:** -- `1:N` with `accounts` -- `1:N` with `settings` -**Target Commodity model:** +- `N:1` with `users` +- `N:1` with `commodities` +- `1:N` with `operations` -| Field | Type | Description | Constraints | -|------|-----|----------|-------------| -| `id` | UUID | Stable Commodity identity | PK | -| `userId` | UUID | Commodity owner | FK -> `users.id`, NOT NULL | -| `code` | String | Display/search code | NOT NULL, unique per user | -| `name` | String | Display name | NOT NULL | -| `symbol` | String | Display symbol | NOT NULL | -| `precision` | Integer | Minor-unit scale for integer amounts | NOT NULL, immutable after creation | -| `isArchived` | Boolean | Hidden from new assignments | NOT NULL, default: false | -| `createdAt` | Timestamp | Creation time | NOT NULL | -| `updatedAt` | Timestamp | Last update time | NOT NULL | +**Notes:** + +- Account Commodity is immutable after account creation. +- Money amounts are stored as integer minor units to avoid floating-point + issues. +- System accounts are reserved for future trading-account support. --- -### 📝 **Transactions** +### Transactions + Top-level grouping of related financial events. | Field | Type | Description | Constraints | |------|-----|----------|-------------| | `id` | UUID | Primary key | PK, NOT NULL | +| `valuationCommodityId` | UUID | Commodity that denominates operation `value` fields | FK -> `commodities.id`, NOT NULL | | `description` | String | Transaction description | NOT NULL | -| `currency` | String | Base currency of the transaction | NOT NULL (CurrencyCode) | | `transactionDate` | Date | Transaction date | NOT NULL | | `postingDate` | Date | Posting date | NOT NULL | | `version` | Integer | Optimistic concurrency version | NOT NULL, default: 0 | | `isTombstone` | Boolean | Soft delete flag | NOT NULL, default: false | -| `userId` | UUID | Transaction owner | FK → `users.id` | +| `userId` | UUID | Transaction owner | FK -> `users.id`, NOT NULL, ON DELETE CASCADE | | `createdAt` | Timestamp | Creation date | NOT NULL | | `updatedAt` | Timestamp | Last update date | NOT NULL | **Relations:** + - `N:1` with `users` +- `N:1` with `commodities` - `1:N` with `operations` (cascade delete) **Indexes:** -- `idx_transactions_user_date` on `(userId, transactionDate)` + +- `idx_transactions_user_date` on `(userId, transactionDate)`. **Notes:** -- `currency` is the base/reporting currency for the entire transaction; used to compute `value` on each operation -- `version` supports optimistic concurrency control—increment on every update + +- `valuationCommodityId` determines the denomination of operation `value`. +- `version` supports optimistic concurrency control and increments on aggregate + updates. --- -### 🔄 **Operations** +### Operations + Individual financial postings affecting accounts. | Field | Type | Description | Constraints | |------|-----|----------|-------------| | `id` | UUID | Primary key | PK, NOT NULL | -| `transactionId` | UUID | Parent transaction | FK → `transactions.id` | -| `accountId` | UUID | Affected account | FK → `accounts.id` | -| `amount` | Integer | Amount in **account's** currency (cents) | NOT NULL | -| `value` | Integer | Amount in **transaction's** currency (cents) | NOT NULL | -| `description` | String | Operation description | NULLABLE | -| `isSystem` | Boolean | System operation flag (for trading ops) | NOT NULL, default: false | +| `transactionId` | UUID | Parent transaction | FK -> `transactions.id`, NOT NULL, ON DELETE CASCADE | +| `accountId` | UUID | Affected account | FK -> `accounts.id`, NOT NULL, ON DELETE RESTRICT | +| `amount` | Integer | Amount in the account's Commodity minor units | NOT NULL | +| `value` | Integer | Amount in the transaction valuation Commodity minor units | NOT NULL | +| `description` | String | Operation description | NOT NULL | +| `isSystem` | Boolean | Reserved system operation flag | NOT NULL, default: false | | `isTombstone` | Boolean | Soft delete flag | NOT NULL, default: false | -| `userId` | UUID | Operation owner | FK → `users.id` | +| `userId` | UUID | Operation owner | FK -> `users.id`, NOT NULL, ON DELETE CASCADE | | `createdAt` | Timestamp | Creation date | NOT NULL | | `updatedAt` | Timestamp | Last update date | NOT NULL | **Relations:** -- `N:1` with `transactions` (cascade delete) -- `N:1` with `accounts` (restrict delete) + +- `N:1` with `transactions` +- `N:1` with `accounts` - `N:1` with `users` **Business Rules:** -- `amount` is always in the account's native currency -- `value` is in the transaction's base currency (equal to `amount` for same-currency transactions) -- Positive = debit (increases Asset/Expense, decreases Liability/Income) -- Negative = credit (decreases Asset/Expense, increases Liability/Income) -- System operations (trading operations) have `isSystem = true` -- Balance check: `sum(value)` across all operations in a transaction must equal 0 + +- `amount` is denominated by the operation account's Commodity. +- `value` is denominated by the parent transaction's valuation Commodity. +- Balance check: `sum(value)` across active operations in a transaction must + equal 0. +- System operations are reserved for future trading-account support. **Indexes:** -- `idx_operations_transaction` on `transactionId` -- `idx_operations_account` on `accountId` -- `idx_operations_user` on `userId` + +- `idx_operations_transaction` on `transactionId`. +- `idx_operations_account` on `accountId`. +- `idx_operations_user` on `userId`. --- -### ⚙️ **Settings** +### Settings + User application settings. | Field | Type | Description | Constraints | |------|-----|----------|-------------| -| `userId` | UUID | User | PK, FK → `users.id` | -| `baseCurrency` | String | Base currency for reporting | FK → `currencies.code`, default: 'RUB' | +| `userId` | UUID | User | FK -> `users.id`, NOT NULL | | `createdAt` | Timestamp | Creation date | NOT NULL | | `updatedAt` | Timestamp | Last update date | NOT NULL | **Relations:** -- `1:1` with `users` -- `N:1` with `currencies` + +- `N:1` with `users` + +**Notes:** + +- Settings do not currently store a base reporting Commodity. --- -## 🔗 Relationships +## Relationships ### Data Hierarchy -``` + +```text USERS (root entity) -├── ACCOUNTS (financial accounts) -├── TRANSACTIONS (financial events) -│ └── OPERATIONS (individual postings) -└── SETTINGS (user preferences) +|- COMMODITIES (user-owned monetary units) +|- ACCOUNTS (financial accounts) +|- TRANSACTIONS (financial events) +| `- OPERATIONS (individual postings) +`- SETTINGS (user preferences) ``` ### Entity Relations -- **Users ↔ Accounts**: `1:N` with cascade delete -- **Users ↔ Transactions**: `1:N` with cascade delete -- **Users ↔ Operations**: `1:N` with cascade delete -- **Users ↔ Settings**: `1:1` -- **Transactions ↔ Operations**: `1:N` with cascade delete -- **Accounts ↔ Operations**: `1:N` (restrict delete) -- **Currencies ↔ Accounts**: `1:N` -- **Currencies ↔ Settings**: `1:N` + +- **Users <-> Commodities**: `1:N` with cascade delete. +- **Users <-> Accounts**: `1:N` with cascade delete. +- **Users <-> Transactions**: `1:N` with cascade delete. +- **Users <-> Operations**: `1:N` with cascade delete. +- **Users <-> Settings**: `1:N`. +- **Commodities <-> Accounts**: `1:N`. +- **Commodities <-> Transactions**: `1:N`. +- **Transactions <-> Operations**: `1:N` with cascade delete. +- **Accounts <-> Operations**: `1:N` with restrict delete. --- -## 💡 Business Rules +## Business Rules ### Uniqueness -1. **User email** must be unique system-wide -2. **Account name** must be unique per user + +1. **User email** must be unique system-wide. +2. **Commodity code** must be unique per user. +3. **Account name** must be unique per user. ### Data Integrity -1. **Cascade deletions**: When a user is deleted, all their data is deleted -2. **Required relations**: Each operation must have a transaction and account -3. **Currency constraints**: All accounts reference existing currencies (application-level) -4. **Soft deletes**: Entities use `isTombstone` flag instead of hard deletes + +1. **User ownership**: accounts and transactions may reference only Commodities + owned by the same user. +2. **Required relations**: each operation must have a transaction and account. +3. **Soft deletes**: entities use `isTombstone` where the deletion must be + retained in storage. +4. **Commodity precision**: `precision` is immutable after Commodity creation. ### Accounting Principles -1. **Double-entry bookkeeping**: `sum(value)` across all operations in a transaction must equal 0 -2. **Multi-currency support**: Handled through `amount` (account currency) + `value` (transaction currency) and trading accounts -3. **Immutable operations**: Operations cannot be edited, only recreated on transaction updates + +1. **Double-entry bookkeeping**: `sum(value)` across active operations in a + transaction must equal 0. +2. **Commodity denomination**: `amount` is in account Commodity units; `value` + is in transaction valuation Commodity units. +3. **Immutable operations**: operations cannot be edited, only recreated on + transaction updates. --- -## 🚀 Schema Files +## Schema Files Schemas are defined in the following files: + - `apps/backend/src/db/schemas/users.ts` - Users +- `apps/backend/src/db/schemas/commodities.ts` - Commodities - `apps/backend/src/db/schemas/accounts.ts` - Accounts -- `apps/backend/src/db/schemas/currencies.ts` - Currencies - `apps/backend/src/db/schemas/transactions.ts` - Transactions - `apps/backend/src/db/schemas/operations.ts` - Operations - `apps/backend/src/db/schemas/settings.ts` - Settings @@ -327,4 +369,4 @@ All schemas are exported through `apps/backend/src/db/schema.ts`. --- -*Last updated: March 11, 2026* +*Last updated: July 23, 2026* diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index f7bcf135..0900e24f 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -30,8 +30,8 @@ Represents a single financial posting affecting an account. - Links to a transaction (`transactionId`) and an account (`accountId`) - Belongs to a user (`userId`) -- `amount` — signed integer in the **account's native currency** (cents) -- `value` — signed integer in the **transaction's currency** (cents); used for balance validation +- `amount` — signed integer in the **account's Commodity** minor units +- `value` — signed integer in the **transaction valuation Commodity** minor units; used for balance validation - `amount` and `value` must be valid integer minor-unit values. `NaN`, `Infinity`, missing values, and decimal/floating-point values are invalid. Zero is allowed and is not rejected by the domain model. @@ -55,7 +55,7 @@ Represents different financial accounts with unified structure for all account t - **Liability**: Debts, loans, credit - **Income**: Revenue sources (salary, interest) - **Expense**: Spending categories -- Has a designated currency +- Has a designated Commodity - **Balance tracking**: - For Asset/Liability: real balance stored in `currentClearedBalanceLocal`, must match reality - For Income/Expense: reporting metric (sum over period), calculated from operations @@ -66,12 +66,10 @@ Represents different financial accounts with unified structure for all account t ### Commodity -Represents a user-owned monetary unit used in the system. - -> Design note: the current implementation still uses currency strings in some -> places, but the target domain model is a user-owned Commodity Registry. -> Commodity identity is a stable id, not `code`. See -> [ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). +Represents a user-owned monetary unit used in the system. Commodity identity is +a stable id, not `code`; display codes such as `USD`, `EUR`, or `RUB` are +metadata owned by each user. See +[ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). #### Key Properties @@ -81,7 +79,7 @@ Represents a user-owned monetary unit used in the system. - Operations always store `amount` in the account's Commodity - Commodity `code`, `name`, and `symbol` are display metadata, not identity - Commodity `precision` defines integer minor-unit interpretation -- Archived Commodities remain readable but cannot be used for new assignments +- Commodity soft deletion is represented by technical `isTombstone` state ## Entity API Conventions @@ -236,9 +234,9 @@ tasks. can be a valid transaction when the transaction-level balance rule is satisfied; there is no separate "non-zero net effect per account" invariant. -### Currency Handling +### Commodity Handling -1. Each operation carries both `amount` (account currency) and `value` (transaction currency) +1. Each operation carries both `amount` (account Commodity) and `value` (transaction valuation Commodity) 2. For same-currency operations `amount === value` 3. **Trading operations** (`isSystem = true`) and system trading accounts are **not currently implemented**; they are reserved for a future multi-currency reconciliation phase @@ -343,6 +341,7 @@ Balance: `sum(value) = 0` ✓ — currently this phase is not implemented. ``` Transaction - id: UUID +- valuationCommodityId: UUID (FK) -- denominates operation value fields - description: string - transactionDate: date (ISO string) - postingDate: date (ISO string) @@ -356,8 +355,8 @@ Operation - id: UUID - transactionId: UUID (FK) -- directly linked to Transaction (Entry removed) - accountId: UUID (FK) -- amount: integer -- in account's native currency (cents) -- value: integer -- in transaction's currency (cents); used for balance validation +- amount: integer -- in account Commodity minor units +- value: integer -- in transaction valuation Commodity minor units; used for balance validation - description: string (optional) - isSystem: boolean -- reserved for future trading operations (currently always false) - isTombstone: boolean @@ -369,7 +368,7 @@ Account - id: UUID - name: string - type: enum (Asset, Liability, Income, Expense) -- currency: string (FK) +- commodityId: UUID (FK) - description: string - initialBalance: integer (cents) - currentClearedBalanceLocal: integer (cents) @@ -379,20 +378,19 @@ Account - createdAt: timestamp - updatedAt: timestamp -Commodity (target model, see ADR 0006) +Commodity - id: UUID - userId: UUID (FK) - code: string -- display/search value, unique per user - name: string -- symbol: string +- symbol: string (optional) - precision: integer -- immutable after creation in the MVP -- isArchived: boolean +- isTombstone: boolean - createdAt: timestamp - updatedAt: timestamp Settings -- userId: UUID (PK, FK) -- baseCurrency: string (FK, legacy; target model should use Commodity id) +- userId: UUID (FK) - createdAt: timestamp - updatedAt: timestamp ``` @@ -425,7 +423,7 @@ Settings - Schema validation (Zod) - Domain validation (business rules) - Database constraints (Drizzle) -2. Branded types for CurrencyCode and other primitive domain values where useful +2. Branded types for Commodity codes, Commodity precision, and other primitive domain values where useful 3. Operation hash-based idempotent updates 4. Enhanced error handling with domain-specific errors @@ -457,7 +455,7 @@ Settings - Domain business accessors expose active operations by default 6. **Type safety**: - Branded types for dates (`IsoDatetimeString`) - - Planned branded types for `CurrencyCode` and other primitive domain values where useful + - Branded types for Commodity codes, Commodity precision, dates, and other primitive domain values where useful - Strict TypeScript configuration - Error handling - Response serialization diff --git a/docs/MULTICURRENCY_DESIGN.md b/docs/MULTICURRENCY_DESIGN.md index 9d70ebde..39ec76c7 100644 --- a/docs/MULTICURRENCY_DESIGN.md +++ b/docs/MULTICURRENCY_DESIGN.md @@ -128,16 +128,14 @@ Trading accounts will never be displayed to the user. ## 5. Data Structure -> Design note: the current schema uses `currency` strings, but the target model -> is a user-owned Commodity Registry. Commodity identity is a stable id, not a -> display code. See -> [ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). +Ledgerly stores monetary-unit identity through user-owned Commodities. +Commodity identity is a stable id, not a display code. See +[ADR 0006](./architecture/adr/0006-commodity-registry-before-currency-validation.md). ### Transaction Top-level entity representing a financial event: - `id`, `userId`, `description` -- `currency` today; target model: `valuationCommodityId`, which determines - `value` denomination +- `valuationCommodityId`, which determines `value` denomination - `transactionDate`, `postingDate` - `version` (optimistic concurrency) - `createdAt`, `updatedAt`, `isTombstone` @@ -145,8 +143,8 @@ Top-level entity representing a financial event: ### Operation Individual account posting: - `id`, `transactionId`, `accountId`, `userId` -- `amount` (signed integer, in **account's currency**) -- `value` (signed integer, in **transaction's currency**) +- `amount` (signed integer, in the **account's Commodity**) +- `value` (signed integer, in the **transaction valuation Commodity**) - `description` (optional) - `isSystem` (true for trading postings) - `createdAt`, `updatedAt`, `isTombstone` @@ -155,10 +153,10 @@ Individual account posting: This follows the standard GnuCash split model: -| Field | Currency | Purpose | -|-------|----------|---------| -| `amount` | Account's native currency | How much was posted to this account | -| `value` | Transaction's currency | The equivalent amount in the transaction's denomination | +| Field | Denomination | Purpose | +|-------|--------------|---------| +| `amount` | Account's Commodity | How much was posted to this account | +| `value` | Transaction valuation Commodity | The equivalent amount in the transaction's denomination | For **same-currency** transactions both fields are equal. @@ -174,7 +172,7 @@ Transaction currency: USD **Balance validation** is performed on `value`: the sum of `value` across all operations of a transaction must equal zero, regardless of how many currencies are involved. Notes: -- Currency is determined by the account. +- Account denomination is determined by the account's Commodity. - Operations are immutable. - Normal operations have `isSystem = false`. - Trading operations have `isSystem = true`. diff --git a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md index 3d978347..9d979723 100644 --- a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md +++ b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md @@ -8,9 +8,9 @@ ## Context LED-47 asks for transaction currency consistency: a transaction currency must be -present and must exist in the system. The current database has a `currencies` -table keyed by currency code, while `accounts.currency` and -`transactions.currency` are plain text fields without foreign keys. +present and must exist in the system. At the time of this decision, the database +had a `currencies` table keyed by currency code, while `accounts.currency` and +`transactions.currency` were plain text fields without foreign keys. That model is too narrow for the planned Ledgerly domain. Ledgerly is inspired by GnuCash, where monetary units are modeled as commodities. Fiat currencies are @@ -42,9 +42,8 @@ The Commodity model is: - `code` is unique only within one user's Commodity registry; - `precision` defines integer minor-unit interpretation and is immutable after Commodity creation in the MVP; -- Commodity can be archived; archived Commodities cannot be assigned to new - accounts or new transaction valuation units, but existing references remain - readable. +- Commodity supports technical soft deletion through `isTombstone` in the MVP; + user-facing archiving is out of scope for the initial Commodity registry. Domain references should move from currency strings to Commodity ids: @@ -94,7 +93,7 @@ Those can be introduced by later ADRs if needed. ## Consequences - LED-47 is re-scoped: final consistency validation must use Commodity - existence, ownership, and archive state instead of `currencies.code`. + existence, ownership, and tombstone state instead of `currencies.code`. - The current `Currency` value object should stop being domain identity. During migration it may survive only as a primitive code/format helper if useful. - Persistence needs a `commodities` model and a migration path from existing From 8d525e129ee80d9f817e1274d27989c0bb6a56f4 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 09:58:18 +0300 Subject: [PATCH 12/30] LED-66 refactor(types): remove isSystem from AccountCreateDTO and update related schemas --- .../mappers/account.mapper.test.ts | 2 -- .../transaction-read-model-response.mapper.ts | 1 + .../mappers/transaction.mapper.test.ts | 1 + .../application/mappers/transaction.mapper.ts | 1 + .../read-models/transaction.read-model.ts | 1 + .../__tests__/getAllTransactions.test.ts | 1 + .../__tests__/getTransactionById.test.ts | 3 +++ .../transaction-query.repository.test.ts | 1 + .../transaction-read-model.mapper.ts | 1 + apps/frontend/tsconfig.app.tsbuildinfo | 2 +- packages/shared/src/types/account.ts | 3 +-- packages/shared/src/types/transaction.ts | 1 + packages/shared/src/validation/accounts.ts | 13 ----------- .../shared/src/validation/transactions.ts | 22 +------------------ 14 files changed, 14 insertions(+), 39 deletions(-) diff --git a/apps/backend/src/application/mappers/account.mapper.test.ts b/apps/backend/src/application/mappers/account.mapper.test.ts index 62745e4f..84d69d3e 100644 --- a/apps/backend/src/application/mappers/account.mapper.test.ts +++ b/apps/backend/src/application/mappers/account.mapper.test.ts @@ -41,7 +41,6 @@ describe('AccountMapper', () => { it('maps a full update DTO to domain update props', () => { const dto: AccountUpdateDTO = { description: 'Updated description', - isSystem: false, name: 'Updated Account', type: 'liability', }; @@ -59,7 +58,6 @@ describe('AccountMapper', () => { }; expect(AccountMapper.toUpdateProps(dto)).toEqual({ - currency: undefined, description: undefined, name: dto.name, type: undefined, diff --git a/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts b/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts index c974ad1a..17734c11 100644 --- a/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts +++ b/apps/backend/src/application/mappers/transaction-read-model-response.mapper.ts @@ -6,6 +6,7 @@ export class TransactionReadModelResponseMapper { transaction: TransactionReadModel, ): TransactionResponseDTO { return { + commodityId: transaction.commodityId, createdAt: transaction.createdAt, description: transaction.description, id: transaction.id, diff --git a/apps/backend/src/application/mappers/transaction.mapper.test.ts b/apps/backend/src/application/mappers/transaction.mapper.test.ts index 7da5bd07..b86f0bc3 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.test.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.test.ts @@ -63,6 +63,7 @@ describe('TransactionMapper', () => { const response = TransactionMapper.toResponseDTO(transaction); expect(response).toEqual({ + commodityId: transactionSnapshot.commodityId, createdAt: transactionSnapshot.createdAt, description: transactionSnapshot.description, id: transactionSnapshot.id, diff --git a/apps/backend/src/application/mappers/transaction.mapper.ts b/apps/backend/src/application/mappers/transaction.mapper.ts index 8e3fe52d..c44ec135 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.ts @@ -15,6 +15,7 @@ export class TransactionMapper { const snapshot = transaction.toActiveSnapshot(); return { + commodityId: snapshot.commodityId, createdAt: snapshot.createdAt, description: snapshot.description, id: snapshot.id, diff --git a/apps/backend/src/application/read-models/transaction.read-model.ts b/apps/backend/src/application/read-models/transaction.read-model.ts index 6e3d755e..61e3e422 100644 --- a/apps/backend/src/application/read-models/transaction.read-model.ts +++ b/apps/backend/src/application/read-models/transaction.read-model.ts @@ -27,5 +27,6 @@ export type TransactionReadModel = { transactionDate: IsoDateString; updatedAt: IsoDatetimeString; userId: UUID; + commodityId: UUID; version: number; }; diff --git a/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts index 9457fd5a..8903aa97 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/getAllTransactions.test.ts @@ -57,6 +57,7 @@ describe('GetAllTransactionsUseCase', () => { const transactionId = Id.create().valueOf(); return { + commodityId: Id.create().valueOf(), createdAt: Timestamp.create().valueOf(), description: 'Test transaction', id: transactionId, diff --git a/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts index e62b5f6e..f286e251 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/getTransactionById.test.ts @@ -29,6 +29,7 @@ describe('GetTransactionByIdUseCase', () => { const userId = Id.create().valueOf(); const createdAt = Timestamp.create().valueOf(); const updatedAt = Timestamp.create().valueOf(); + const commodityId = Id.create().valueOf(); const mockOperation1: OperationReadModel = { accountId: Id.create().valueOf(), @@ -59,6 +60,7 @@ describe('GetTransactionByIdUseCase', () => { const operations = [mockOperation1, mockOperation2]; const mockTransactionData: TransactionReadModel = { + commodityId, createdAt, description: 'Test Transaction', id: transactionId, @@ -85,6 +87,7 @@ describe('GetTransactionByIdUseCase', () => { expect(transaction.version).toBe(mockTransactionData.version); expect(Object.keys(transaction).sort()).toEqual( [ + 'commodityId', 'createdAt', 'description', 'id', diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts index 5d7cfbbc..3ae96822 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-query.repository.test.ts @@ -485,6 +485,7 @@ describe('TransactionQueryRepository', () => { expect(Object.keys(transaction).sort()).toEqual( [ + 'commodityId', 'createdAt', 'description', 'id', diff --git a/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts b/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts index 4f6f2c0d..0ed86d79 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction-read-model.mapper.ts @@ -9,6 +9,7 @@ export class TransactionReadModelMapper { transaction: TransactionWithRelations, ): TransactionReadModel { return { + commodityId: transaction.commodityId, createdAt: transaction.createdAt, description: transaction.description, id: transaction.id, diff --git a/apps/frontend/tsconfig.app.tsbuildinfo b/apps/frontend/tsconfig.app.tsbuildinfo index cd28048f..23e92903 100644 --- a/apps/frontend/tsconfig.app.tsbuildinfo +++ b/apps/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/client.d.ts","../../node_modules/.pnpm/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.d.ts","../../node_modules/.pnpm/tiny-warning@1.0.3/node_modules/tiny-warning/src/index.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/types.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/store.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/derived.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/effect.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/scheduler.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/fileroute.d.ts","../../node_modules/.pnpm/@tanstack+history@1.115.0/node_modules/@tanstack/history/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/utils.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/location.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/link.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/routeinfo.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/not-found.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/matches.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/root.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/routerprovider.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/route.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/validators.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/searchparams.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/redirect.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/manifest.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/serializer.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/router.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/defer.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/path.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/qss.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/searchmiddleware.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/structuralsharing.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useroutecontext.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/usesearch.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useparams.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/usenavigate.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useloaderdeps.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useloaderdata.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/scroll-restoration.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/typeprimitives.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/serializer.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/awaited.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/structuralsharing.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useloaderdata.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usematch.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useloaderdeps.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useparams.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usesearch.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useroutecontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/typeprimitives.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/link.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/route.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/catchboundary.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/clientonly.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/fileroute.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/history.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/lazyroutecomponent.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/matches.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/matchcontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/match.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/router.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/routerprovider.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scrollrestoration.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useblocker.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usenavigate.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/routercontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/userouter.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/userouterstate.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/uselocation.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usecangoback.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/utils.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/not-found.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scriptonce.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/asset.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/headcontent.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scripts.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/index.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/scheduler.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/jsx.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/component.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/flow.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/suspense.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/hydration.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/index.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/signal.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/observable.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/array.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/index.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/tanstackrouterdevtools.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/tanstackrouterdevtoolspanel.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/typealiases.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/util.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/zoderror.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/locales/en.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/errors.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/parseutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/enumutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/errorutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/partialutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/standard-schema.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/types.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/external.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/index.d.ts","../../packages/shared/src/validation/basevalidations.ts","../../packages/shared/src/types/types.ts","../../packages/shared/src/types/account.ts","../../packages/shared/src/types/user.ts","../../packages/shared/src/constants/accounts.ts","../../packages/shared/src/constants/currencies.ts","../../packages/shared/src/constants/operations.ts","../../packages/shared/src/constants/index.ts","../../packages/shared/src/validation/accounts.ts","../../packages/shared/src/validation/transactions.ts","../../packages/shared/src/validation/users.ts","../../packages/shared/src/validation/auth.ts","../../packages/shared/src/validation/index.ts","../../packages/shared/src/types/auth.ts","../../packages/shared/src/types/index.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/global.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/errors.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/decorator_fills.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/decorators.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/atom.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/comparer.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/modifiers.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/overrideannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/actionannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/flowannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/computedannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/autoannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/generic-abort-signal.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/observable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/computed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/action.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablevalue.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/computedvalue.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/derivation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/globalstate.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/observable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/reaction.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableobject.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablearray.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/spy.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/action.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/autorun.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/become-observed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/configure.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/extendobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/extras.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/flow.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/intercept-read.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/intercept.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/iscomputed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/isobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/object-api.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/observe.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/tojs.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/trace.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/transaction.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/when.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/dynamicobject.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/intercept-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/listen-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/makeobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablemap.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableset.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/legacyobservablearray.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/type-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/eq.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/iterable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/annotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/internal.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/mobx.d.ts","../../packages/shared/src/routes/index.ts","./src/shared/config/config.ts","./src/shared/api/config.ts","./src/shared/api/index.ts","./src/entities/currencies/api/currenciesactions.ts","./src/entities/currencies/model/currencystate.ts","./src/entities/currencies/index.ts","../../node_modules/.pnpm/lucide-react@0.507.0_react@19.1.0/node_modules/lucide-react/dist/lucide-react.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/assertenvironment.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/universalfinalizationregistry.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/observerfinalizationregistry.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/staticrendering.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/observer.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/observercomponent.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/uselocalobservable.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/uselocalstore.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/useasobservablesource.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/observerbatching.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/index.d.ts","../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/clsx.d.mts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/closebutton/closebutton.tsx","./src/shared/ui/closebutton/index.ts","./src/shared/ui/modal/modal.tsx","./src/features/confirmdialog/model/confirmdialogstate.ts","./src/features/confirmdialog/ui/confirmdialog.tsx","./src/features/confirmdialog/index.ts","./src/shared/ui/layout/footer/footer.tsx","./src/shared/ui/layout/footer/index.ts","./src/shared/ui/layout/header/header.tsx","./src/shared/ui/layout/header/index.ts","./src/shared/ui/layout/main/main.tsx","./src/shared/ui/layout/main/index.ts","./src/shared/lib/modalstate/modalstate.ts","./src/shared/lib/modalstate/index.ts","./src/entities/accounts/api/accountactions.ts","./src/entities/accounts/model/accountsstate.ts","./src/entities/accounts/ui/accountslist/accountslist.tsx","./src/entities/accounts/ui/accountslist/index.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/constants.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/createsubject.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/events.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/common.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/eager.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/fieldarray.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/resolvers.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/form.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/utils.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/fields.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/errors.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/validator.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/controller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/controller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/form.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/appenderrors.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/createformcontrol.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usecontroller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usefieldarray.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useform.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useformcontext.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useformstate.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usewatch.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/get.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/set.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/index.d.ts","../../node_modules/.pnpm/@hookform+resolvers@5.0.1_react-hook-form@7.56.4_react@19.1.0_/node_modules/@hookform/resolvers/zod/dist/zod.d.ts","../../node_modules/.pnpm/@hookform+resolvers@5.0.1_react-hook-form@7.56.4_react@19.1.0_/node_modules/@hookform/resolvers/zod/dist/index.d.ts","./src/features/manageaccount/ui/manageaccountform.tsx","./src/features/manageaccount/ui/manageaccountmodal.tsx","./src/features/manageaccount/ui/index.ts","./src/shared/ui/layout/sidebar/sidebar.tsx","./src/shared/ui/layout/layout.tsx","./src/shared/ui/layout/index.ts","./src/routes/__root.tsx","./src/routes/transactions.tsx","./src/routes/budget.tsx","./src/routes/about.tsx","./src/pages/indexpage/indexpage.tsx","./src/pages/indexpage/index.ts","./src/routes/index.tsx","./src/pages/accountspage/accountspage.tsx","./src/pages/accountspage/index.ts","./src/routes/accounts/index.tsx","./src/pages/accountpage/accountpage.tsx","./src/pages/accountpage/index.ts","./src/routes/accounts/$accountid.tsx","./src/routetree.gen.ts","./src/main.tsx","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4/node_modules/vite/types/hmrpayload.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4/node_modules/vite/types/customevent.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4/node_modules/vite/types/hot.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4/node_modules/vite/types/importglob.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4/node_modules/vite/types/importmeta.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4/node_modules/vite/client.d.ts","./src/vite-env.d.ts","./src/entities/transactions/api/transactionsactions.ts","./src/entities/transactions/model/transactionsstate.ts","./src/shared/types/index.ts","./src/shared/ui/layout/sidebar/index.ts","./src/shared/ui/modal/index.ts","./src/shared/ui/navbar/navbarconfig.ts","./src/shared/ui/navbar/navbar.tsx","./src/shared/ui/navbar/index.ts","./src/widgets/dashboard/dashboard.tsx","./src/widgets/dashboard/index.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/types.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/index.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/index.d.ts","../../packages/shared/src/libs/formatters.ts","../../packages/shared/src/libs/index.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/index.d.ts"],"fileIdsList":[[51,171,229,232,350,392],[51,171,228,264,265,350,392],[50,51,128,171,236,247,250,266,320,321,350,392],[51,267,350,392],[51,234,350,392],[51,171,228,233,350,392],[51,350,392],[51,254,255,350,392],[51,228,350,392],[51,236,247,250,253,254,350,392],[51,302,350,392],[51,164,169,171,235,247,266,298,300,350,392],[51,247,250,253,256,266,301,350,392],[50,51,52,128,320,321,327,350,392],[51,128,320,321,350,392],[51,317,350,392],[51,314,350,392],[51,311,350,392],[51,128,306,320,321,350,392],[51,128,318,320,321,350,392],[51,128,315,320,321,350,392],[51,128,312,320,321,350,392],[51,128,307,308,309,310,313,316,319,321,350,392],[51,230,350,392],[51,231,350,392],[51,263,350,392],[50,51,248,350,392],[51,249,350,392],[51,236,350,392],[51,251,350,392],[51,257,350,392],[51,259,350,392],[51,305,350,392],[50,51,142,235,256,258,260,262,304,350,392],[51,261,350,392],[51,304,350,392],[51,250,266,268,303,350,392],[50,51,248,252,350,392],[51,335,350,392],[51,128,320,321,334,350,392],[327,350,392],[51,337,350,392],[299,350,392],[156,298,350,392],[350,392],[140,141,350,392],[50,128,139,320,321,350,392],[50,128,320,321,350,392],[91,92,103,109,112,350,392],[50,51,91,92,103,109,112,350,392],[50,51,103,350,392],[50,51,350,392],[91,92,95,96,97,98,99,100,103,109,112,350,392],[51,91,92,103,109,112,350,392],[62,350,392],[53,54,62,91,92,93,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,350,392],[103,350,392],[50,91,92,101,103,109,112,350,392],[50,350,392],[50,51,91,92,94,103,109,112,350,392],[50,91,92,95,96,97,98,99,100,102,103,109,112,350,392],[62,91,92,103,107,109,112,350,392],[50,91,92,103,109,112,350,392],[50,91,103,109,112,350,392],[91,92,98,99,102,103,109,112,350,392],[50,62,91,92,103,107,109,112,350,392],[91,92,94,103,109,112,350,392],[77,350,392],[71,72,350,392],[61,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,350,392],[62,63,64,66,77,107,350,392],[62,72,107,350,392],[63,66,71,77,350,392],[66,77,350,392],[70,71,350,392],[63,65,77,350,392],[61,63,64,65,66,67,68,69,70,72,77,350,392],[61,63,65,71,77,350,392],[60,62,63,64,65,66,67,68,70,71,72,73,74,75,76,78,107,350,392],[64,65,66,77,350,392],[63,64,77,350,392],[63,65,71,350,392],[72,350,392],[63,350,392],[63,65,66,74,77,84,85,350,392],[63,66,77,350,392],[65,77,350,392],[71,350,392],[55,56,350,392],[57,350,392],[55,56,57,58,59,350,392],[56,57,350,392],[55,350,392],[350,389,392],[350,391,392],[392],[350,392,397,427],[350,392,393,398,404,405,412,424,435],[350,392,393,394,404,412],[345,346,347,350,392],[350,392,395,436],[350,392,396,397,405,413],[350,392,397,424,432],[350,392,398,400,404,412],[350,391,392,399],[350,392,400,401],[350,392,404],[350,392,402,404],[350,391,392,404],[350,392,404,405,406,424,435],[350,392,404,405,406,419,424,427],[350,387,392,440],[350,387,392,400,404,407,412,424,435],[350,392,404,405,407,408,412,424,432,435],[350,392,407,409,424,432,435],[348,349,350,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441],[350,392,404,410],[350,392,411,435],[350,392,400,404,412,424],[350,392,413],[350,392,414],[350,391,392,415],[350,389,390,391,392,393,394,395,396,397,398,399,400,401,402,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441],[350,392,417],[350,392,418],[350,392,404,419,420],[350,392,419,421,436,438],[350,392,404,424,425,427],[350,392,426,427],[350,392,424,425],[350,392,427],[350,392,428],[350,389,392,424],[350,392,404,430,431],[350,392,430,431],[350,392,397,412,424,432],[350,392,433],[350,392,412,434],[350,392,407,418,435],[350,392,397,436],[350,392,424,437],[350,392,411,438],[350,392,439],[350,392,397,404,406,415,424,435,438,440],[350,392,424,441],[48,49,350,392],[340,350,392],[339,350,392],[237,239,240,241,242,243,244,245,246,350,392],[228,350,392],[228,238,350,392],[175,227,350,392],[227,350,392],[191,227,350,392],[190,191,196,197,227,350,392],[172,173,174,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,350,392],[50,283,350,392],[283,284,285,288,289,290,291,292,293,294,297,350,392],[283,350,392],[286,287,350,392],[50,281,283,350,392],[278,279,281,350,392],[274,277,279,281,350,392],[278,281,350,392],[50,269,270,271,274,275,276,278,279,280,281,350,392],[271,274,275,276,277,278,279,280,281,282,350,392],[278,350,392],[272,278,279,350,392],[272,273,350,392],[277,279,280,350,392],[277,350,392],[269,274,279,280,350,392],[295,296,350,392],[129,130,135,136,137,138,350,392],[49,350,392],[136,350,392],[129,130,135,350,392],[130,350,392],[130,136,350,392],[131,132,133,134,350,392],[350,359,363,392,435],[350,359,392,424,435],[350,354,392],[350,356,359,392,432,435],[350,392,412,432],[350,392,442],[350,354,392,442],[350,356,359,392,412,435],[350,351,352,355,358,392,404,424,435],[350,359,366,392],[350,351,357,392],[350,359,380,381,392],[350,355,359,392,427,435,442],[350,380,392,442],[350,353,354,392,442],[350,359,392],[350,353,354,355,356,357,358,359,360,361,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,381,382,383,384,385,386,392],[350,359,374,392],[350,359,366,367,392],[350,357,359,367,368,392],[350,358,392],[350,351,354,359,392],[350,359,363,367,368,392],[350,363,392],[350,357,359,362,392,435],[350,351,356,359,366,392],[350,392,424],[350,354,359,380,392,440,442],[326,350,392],[322,350,392],[323,350,392],[324,325,350,392],[155,350,392],[145,146,350,392],[143,144,145,147,148,153,350,392],[144,145,350,392],[154,350,392],[145,350,392],[143,144,145,148,149,150,151,152,350,392],[143,144,155,350,392],[171,350,392],[161,162,163,350,392],[341,350,392],[342,350,392],[158,350,392],[156,169,350,392],[158,159,160,170,350,392],[156,157,350,392],[156,157,164,350,392],[156,350,392],[157,165,166,167,168,350,392]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"742d4b7b02ffc3ba3c4258a3d196457da2b3fec0125872fd0776c50302a11b9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"03566a51ebc848dec449a4ed69518e9f20caa6ac123fa32676aaaabe64adae8e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"88e9caa9c5d2ba629240b5913842e7c57c5c0315383b8dc9d436ef2b60f1c391","impliedFormat":1},{"version":"b8903e4347ba78328892f8c30b45a8a0071e06c7bae0b0a621035fbf36f75bcb","impliedFormat":99},{"version":"5771ec76e11a5e8303d8945e8050daee051436150d5285dc818c8df3239f45bb","impliedFormat":1},{"version":"7f9ef84dcfff2dbd0c8ed9f4ea18216619527f5b79df115a430114ea5a77c3f6","impliedFormat":99},{"version":"368514ed24d7fa3dbbd3360ee9aac4a1e629969268276e92b86c0f672f3fbbb3","impliedFormat":99},{"version":"c2845147e33e886027684d887ec5ddfccf5bc16bc5fd2ec0f244883d5cee4318","impliedFormat":99},{"version":"ead2432b3eb851e40625006ccba0cb39325fa7f72fdbaedd19825bff05722f8e","impliedFormat":99},{"version":"a1ae11570949b69e4a4e0ee1adcd109f3258df90ec3c5d4345d276e629d38366","impliedFormat":99},{"version":"c7c33f7321ee10856267aaedfd6b61cf249e608a6a37ea7204ec84d48f9a1e4b","impliedFormat":99},{"version":"648db4b8b239ed5b5e8c93618bbe4384d69d659ddf040ad200ee4915aee2157e","impliedFormat":99},{"version":"b39c758625e62b1cf23c7cc38bbe561ba53463237a44291486179bc8c4ec138a","impliedFormat":99},{"version":"33686400541314fa6a0757685b354778628768e2b0ef75310027b2d805cbb3aa","impliedFormat":99},{"version":"c630d607581ce676da4f470e1439fa7127bd763a3608a811122a6a1b33264a61","impliedFormat":99},{"version":"d33c779e38298df4fa486c3190beb16194ca42a41947abcb4fc491e6f9944a8c","impliedFormat":99},{"version":"bdfa3b05b56953affb355a81976e6bfb35961ce63b746d6382ce3412c8e0be10","impliedFormat":99},{"version":"169ef5a18d968eefdcdbad749ded93d1127eb40dd78ce0a0e2ecd9d42eea1a1a","impliedFormat":99},{"version":"55a3b2901bf0b3e5c482ea6ae5e34469d9e7242a30a7bf4783c9ffce3cfc8aa3","impliedFormat":99},{"version":"8a30efabf741de93c2884ffdc23f16602042449e28729eb9874953b3ea8dc705","impliedFormat":99},{"version":"ae2845848a8a72c5e211230fc6bdb1b001056836c4ccbed881441ecece9ab23c","impliedFormat":99},{"version":"dc6b45c8b0599be8ca11a2e7e96745072b9f12850fb0eeec7a70486dc9f99712","impliedFormat":99},{"version":"28c6caf88a757b504f8a36901e4cb7571098f92eaf971aaf8ff77101e47a72ac","impliedFormat":99},{"version":"35b4c072ee1aaddc8352e5a40f6d5c7dc15865ff0681cf312fef258362cedfe4","impliedFormat":99},{"version":"88879ddcc4dd4d80d7580d09db58d64e01bdf93def80c4a3eeeb8886f7c3500c","impliedFormat":99},{"version":"0923187df6538b37c811a365ef1d7efa9f1b23c0116fe9255841548311e7b18d","impliedFormat":99},{"version":"5aef5866733347f6fbdd9391e8c2a3c58002472b57c7188912d995e90274a24c","impliedFormat":99},{"version":"19c9ef1751b20765eeb092adbc9f77bf0f5674ef6083bace3598859e629611d8","affectsGlobalScope":true,"impliedFormat":99},{"version":"dcd742a716cc29789bc177f1b3fbf3c8bc2c164b31104635e3e9f84881bf4c2e","impliedFormat":99},{"version":"22568395a672835b8f58b51097b55d914e0939b71b9a8a54754865b590e9ac29","impliedFormat":99},{"version":"b6783f68397f6edf1c6bed6c2d7c0d89c6d443df1f3a6453ced1d4e184e7014c","impliedFormat":99},{"version":"2ee61832845cf9fd22b581a4cf2bb53794fe4043bd1f1c130a5568e6e3e21368","impliedFormat":99},{"version":"aed243366f9c480cd45623a8f2266c2c8868773cdb13db8f46acb2173cd407f1","impliedFormat":99},{"version":"9c372493f8a4e1c6276f1ba3e6521122b65d9de25d34dc8e43effc97fd64be54","impliedFormat":99},{"version":"d90b155a976d4312c36a3dd442bcd52e9326e599c523faae4cfb8942468f5298","impliedFormat":99},{"version":"13876cda8b941c4bff97ae9ddf89f2a04c8e4ff59bf9141f72f892815bf30244","impliedFormat":99},{"version":"f4cc088c0853a0bc7b5c65fcb53422898cb4a412ba46ed6c62716d465a351922","impliedFormat":99},{"version":"65657868876a64f14656e233a5f2e73d354fb824794f934e9543aa7c3aebf51d","impliedFormat":99},{"version":"b220e3999b3cbae6d40e997f4227bc4006ab04f511e5d4636dfd63badeffd025","impliedFormat":99},{"version":"3dab370414f0c44d2e2e0acfd10fff6a57de0ad770c6c26456f31cbf21204f16","impliedFormat":99},{"version":"9b02e7d7ab9800f9c65547f697875e45ea533fd31da0ce06e52798032d3bf2c2","impliedFormat":99},{"version":"65735c7848f6f0cebe410726ec8f647859df9667ccad5972674646f79527d684","impliedFormat":99},{"version":"fb0b313c8baca67889b6048356af082e02de1df625f0e9f8962f397ea3ed560e","impliedFormat":99},{"version":"57eb2b3764a445ce26575a813ca9b8999c7239f70ae435140ab662be599a60f4","impliedFormat":99},{"version":"91a93ec2ea237ab400834525faed8b2214852ee6b080dfc14de4b52d765248b7","impliedFormat":99},{"version":"32485636a4ae3491437b850a2f8ed523c63d59da3ec48375601458c4e59819e4","impliedFormat":99},{"version":"a61b89063f853e9a1e16a029b67588dde050af09a3c3427097f21e9bd5bdd158","impliedFormat":99},{"version":"048375d7ff42f93ed5a29318ac1642a83c02ae0c2fd658f345d1c46e0c54a67f","impliedFormat":99},{"version":"f7a3ce9cc643a4b47429ec47de60555f6ae6877523a2d3f43dd9ef7b0e7556cb","impliedFormat":99},{"version":"eb2e6827dc100235cc062144f983d5851b13a0531a8965a42e71bd5218355141","impliedFormat":99},{"version":"d2e4f966279a90171e6351b1bf6706d14054a4e5bcc8d46948fed172cfe11371","impliedFormat":99},{"version":"32515c5c844173bf500057ab01e2c246cf914d92cd10d0fd4767b0f9591627ae","impliedFormat":99},{"version":"5271013c661bf776d575ce29b8a18565c4674434c70af21f542dffd15d29581f","impliedFormat":99},{"version":"e14172688f53d888134dd0d2c90fd9f0205179db8ab99430d5b1309e50e0cdb8","impliedFormat":99},{"version":"6946f7e29e3bc5cb0da6df5b664d628fb47593f4dc6bdfb141ad5e7d7a4e777b","impliedFormat":99},{"version":"396fb90417ba85a0b114a2b8f7887d68a208293ab7f29b08ae7b7010d2f4de47","impliedFormat":99},{"version":"b9abf61560dc97324a08cdeecbed287440b70c9821d025d483f8ce0ff6907f98","impliedFormat":99},{"version":"d59755c3b9f1ce4bc1379ba54846eaf464b9f4db9026e3cbcd25603cae7562e2","impliedFormat":99},{"version":"a610f9cf754bf9513b44ecb4c50d79928d2d8289b7ef31c67a89b105b3492702","impliedFormat":99},{"version":"7d318d2247741b3cc384060c60e77295ffa7210b1df48eb3e2d1533017af3182","impliedFormat":99},{"version":"f11d572e430c0ea5e957ec85608629c7433b0ad6e0b1450f3e9d91667625fb58","impliedFormat":99},{"version":"d41befd3d4185f93a5ccc1bb099891415b7157d0ede46ae88a20210d4e772bbc","impliedFormat":99},{"version":"c8bbb9ce1c891efe0e45fd16fb5c4f0c06e9ad60ec1e025bb0b161ef71f04371","impliedFormat":99},{"version":"e9124172677261d101c54c55f9025727c0eed6e5c071db507f977b9cfece01ac","impliedFormat":99},{"version":"556a85bb9982f7f12101fbb9a96262f1d7a4ad6bd5cb50db62153438ccf373b5","impliedFormat":99},{"version":"e5af5fb67c619f0f050cea978ed642da92bbbad4d24b9b0373432e7ed3199431","impliedFormat":99},{"version":"2163deee5fbd66077b4fbd4d9cf84c8f06918113eff425a62dc257b051eccd02","impliedFormat":99},{"version":"e8ef79b944df8cbaeed8bcf01062709c91853ea06474571748f65e3a03439656","affectsGlobalScope":true,"impliedFormat":99},{"version":"16db9485a97fb736ec9098a6d1277e7b95fc489c8e8608d83824728f47219d94","impliedFormat":99},{"version":"9e1ad280ec936e7789a3901f29b6ec2b68578ca030ef7c4b51e970d6308aa6e2","impliedFormat":99},{"version":"81f50c2e9a72a15dcfd77a3aff3b676bbf2f270265c3e5ab8d64ba07575417ee","impliedFormat":99},{"version":"038780a3ac1630c2398d24aa98283d23216feebc22a265caa5f747d85b9d0b68","impliedFormat":99},{"version":"3562836bbc8f85daa391c5bac66cc6c85be806b7d1a0bf25bc6897481e555868","impliedFormat":99},{"version":"5dda66d705930bbea2e79e0f0a5b74c5642481c73487e9f48b6e6e2a7535321c","impliedFormat":99},{"version":"fcb569cff31c9ffc80018fe31585a7733d6297977b6d5303a7b0c9c87856e41b","impliedFormat":99},{"version":"616e09e0cc203f41e18848c4e246d248d94518b080e2b16cf28bc705fb9b35b5","impliedFormat":99},{"version":"518dbebaadc0fc21aa92177d30b043613471869c7fc16502b170c184e60fc17a","impliedFormat":99},{"version":"027b6effcb25477fb182104e60037753a7861782db46001ae54d50b574d0f74b","impliedFormat":99},{"version":"4cad4e814e224a5f6cc40c0b5dfd3c60f6715147a94c27568324439325578c35","impliedFormat":99},{"version":"a6d2eb77f390448a229ca63b4adadbcc549d203b8c038f1f7627a118f732969d","impliedFormat":99},{"version":"1ddd07729c458dd68c47ea2ac1f46f33f64f603f53fe43d74c9bcd65487923fd","impliedFormat":99},{"version":"b886cc0d907b31ae7484a85da375bc8ac42f9986ec1e49c9c7c854956b0a928b","impliedFormat":99},{"version":"69029230a28323de141ad3b08350b45468248d1f8cce9fe67e4c9aed7785fb2d","impliedFormat":99},{"version":"8ab288ca80e5f2a9dab19955fcd9e81a244432cd7d256fdb5ea198ed5a44f0a4","impliedFormat":99},{"version":"0caadd121b8965f24d5c647a2ed01f5fc352772e4cea4bbca2ab78adedfdd921","impliedFormat":99},{"version":"b642b6a4b9b670338bc50281a6dd192fb7cc0b2f9d63d38c250bb1f02e828bbc","impliedFormat":99},{"version":"3c52dce9553224361c062550a63e7ea0c60a26352e87e80ce847134ae2e4d782","impliedFormat":99},{"version":"c2e65a549ac9d227a9002d077af67bb1c77557a72bf39a85c4354831879078ed","affectsGlobalScope":true,"impliedFormat":99},{"version":"38f11ad59dfc38750b789f9f250742d0ce0da44e28f790a032cd15665b70f239","impliedFormat":99},{"version":"c86b1f5777f36dd334a630de065b339f04e4ba672ade417039740847c11246a5","affectsGlobalScope":true,"impliedFormat":99},{"version":"38d2308df413ce823f4764af63dc7e9aa64223f6ba71dbbd5559a6695ca06a08","impliedFormat":99},{"version":"01b1fb6368b7f9128e594c8ad67cbfbfeaed998ba9a59091764f37dc13c867d1","impliedFormat":99},{"version":"a5908d37bbe4df084fa9527e9d672f5d69859684c722e465ca18a6b11f666d7b","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"950f2cd81e30d0ecdf70ab78fcfd85fc5bb28b45ebb08c860daff059feea412e","impliedFormat":1},{"version":"3a5af4fba7b27b815bb40f52715aedebaa4b371da3e5a664e7e0798c9b638825","impliedFormat":1},{"version":"8485b6da53ec35637d072e516631d25dae53984500de70a6989058f24354666f","impliedFormat":1},{"version":"ebe80346928736532e4a822154eb77f57ef3389dbe2b3ba4e571366a15448ef2","impliedFormat":1},{"version":"49c632082dc8a916353288d3d8b2dc82b3471794249a381d090d960c8ceac908","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"71addb585c2db7b8e53dc1b0bcfa58c6c67c6e4fa2b968942046749d66f82e7e","impliedFormat":1},{"version":"c76b0c5727302341d0bdfa2cc2cee4b19ff185b554edb6e8543f0661d8487116","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"e703cfacb9965c4d4155346c65a0091ecded90ea98874ed6b3f36286577c4dde","impliedFormat":1},{"version":"f5ef066942e4f0bd98200aa6a6694b831e73200c9b3ade77ad0aa2409e8fe1b1","impliedFormat":1},{"version":"b9e99cd94f4166a245f5158f7286c05406e2a4c694619bceb7a4f3519d1d768e","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"c2c423d1a7d5ee0adaf63e2dc534da01fb1a2ba81b58881e360a7ea1f9c0eeb3","signature":"cc5d57f8575ea2b8977eb191f98847ee210ee2c0c935b1dee029c7435a7f5dd1"},{"version":"98a7074dc846467ca62f2dfd3ab5936b8df76215fde87478740b662fb7c6f58b","signature":"d1aec72fd37c38ca436f5a85f7d330b65b8a754315275f272cbf1e372b100ba4"},{"version":"a4b1ad27e5c15c398bca5bed7120a349d09773f3e936d88f6ad834732216b80f","signature":"f1269d7c09e026cb7ab11c7b664f6b19125eff7ff3af5e5bd54ac9cc8c0352a4"},{"version":"5c043033f251a8c5bd7108914393af575650fa9c78a78e548f81d7cfc4014e6f","signature":"74555895068502b65799c3c9928e42f4e56d6f69b021e6f13f294c1a8c354260"},{"version":"b9b5488f9e8043ff017f648745a75b1c2e030a8efa16c6310fa6e6f9e52bcef5","signature":"58969267c23c27443642dec465082e37d7e210fa71c2cf13f516d876ff97b6fa"},{"version":"5275b12537eadeeac78e852fd6ddebb4882770947c64f06bf1bdaf29ecb61303","signature":"c4a90e38375dfd22ab91d9a420cfa703a5dca4145108e0d1106e9873bc96797b"},{"version":"1af38648bd7056b360f5c3607b305f558a09d1d320fa253a7333c169ea9529b1","signature":"08f9b11447950c4f59141f328520c6e31b5a166924336777995a0187f7a4bde9"},"94a80d8194a2bd89860166870c73abf27aa21cfd5c94a5cb3dedd68f2a431705",{"version":"77bd92a03b913c17a61923ff79652463ea45225d08b0f9b56ee1fae0c25b49d3","signature":"78954cc7b3694f51cf8831576a85948046c93a76ee7d5affc9148bb8b84d6d7a"},{"version":"77f61c89583cd0eb3fc0602b89e4a7bf06c1b85b776a52668bc9145aec09de5d","signature":"a245278a75e5e94dd10da11c606b5b8792797fecde390dbb95b1c59974ee7798"},{"version":"b065b977696599b488831c0189811f198445b31d2684e1017c0dd5a486d7c3ac","signature":"a6785ff8e286cc4b64221c4d9c6a1084f328837d1e3225db9045130dcc8a0ca8"},{"version":"c4a595c3f6c279163c73fde607c056c813ef53f47537bc91b2623dd95f45f2fc","signature":"0dacf73b071d41f5506c1858dbc30a73b85482307cb5e15a63ba4fe654906ce4"},"bb0b00be806f71a00f4a806b9331cc7b919d652d35a53ed9e2774b017dcf1dbd",{"version":"3b96187464c836feb9457f746b1ed44dc91ad76aa65ee8cdd64283b46fda9234","signature":"ec7adc98a93755b7c1ecb861e78423a549a0d8b30751571340bac9c0c974c66a"},"07f93368ac703e3104c9ca84fa0a5ce8f917768b3004712bf9e07465018c215c",{"version":"8c72ef1bb77e1dbfe10f37d22327de08b839246785253756944b94c8a328eece","impliedFormat":1},{"version":"26efb34ac3797956919cecf67aa2c229b875620bb7a4eb16e3ab97e2fafe0c76","impliedFormat":1},{"version":"4b90c2e95674469fb79ad5c7f2fd946304acc2910bf578f7b1929fead92a1d27","impliedFormat":1},{"version":"6ba3836f4607d0e8a2da4d7a47cd9a5750703d69dd07281997c39831a2edbade","impliedFormat":1},{"version":"56024065be958aac94c30535a04a6bd8db908b2924a21011009291353fcd2478","impliedFormat":1},{"version":"ac339e7b2a6953534dc34b2c76b5128c84eb33b947e9c14b0c6f819e8974842f","impliedFormat":1},{"version":"33c337de15324843b658d00995db674f9c618fe9ad6ad6b43f0fe6835984fa0b","impliedFormat":1},{"version":"27254b3eb6ba4c0630413a63892534ecbac47746bed81f72d996dca1662936d4","impliedFormat":1},{"version":"7e687be116d78dd903a427dacc3cbad3ff9f7ace60dffa8c83b7102ded92de47","impliedFormat":1},{"version":"3da682a8d8b2cf020828199e06d40f4b8f617d8582ab466354b804602e492b60","impliedFormat":1},{"version":"cca10549035d257a89fc46649190b96534af65715aa85b57f0d2a5c61fb98938","impliedFormat":1},{"version":"5e04be59bf10e4d8adf988610f7e80864f33e7e6dd5072ce4af354305581dec9","impliedFormat":1},{"version":"6cf6a4290ee17a9104da299a794d39fb02ef19e054ab14f2ec1d2753514f131b","impliedFormat":1},{"version":"06648b76a5d56f77a1443626ced0fe0fa0dc58827df8ad6195e331915a46e347","impliedFormat":1},{"version":"abd934f6207c5954c02630d92160c4d866f0664047dae0a939f9bdcdb8302e98","impliedFormat":1},{"version":"66c15e0c870a2928dc58a7b19762e94a32ff4ebe4a64b42e9419d212e51cf2c8","impliedFormat":1},{"version":"e5d037491332f872acd558856a11b23c4d5bf726ca3cb9208c7ab8051c4b0faa","impliedFormat":1},{"version":"157227cf6b14eee7ee92a96862902be7d64a6bda2befecf6ebf80e97079e1545","impliedFormat":1},{"version":"a4bd311189ce445fa39d86b23905683a6cd6d9eb884bf8a100329b3dbfb1db43","impliedFormat":1},{"version":"f4d06ad1b70434339a4fdf464ac4d804154c71314c3e2fb928bead12b620cf1a","impliedFormat":1},{"version":"c3878889468470079a7a3f91ab157b2c15da1191d9253a3149eedf4b7c7e9712","impliedFormat":1},{"version":"b2ef09a1454cd38bec48a17316abe8c6b1d56711b1071e5974ed5c377741131b","impliedFormat":1},{"version":"56290b22f633dc5afd255ed0e897d1e8e6567fa5abf38773c16ba7b89de4c56a","impliedFormat":1},{"version":"698b7a228b992346acdd678b918804f4e68824fbcb5bfa551603aa6d4217548d","impliedFormat":1},{"version":"dd1a80df85e075e40a7edd6d9a549a2a954956bfbdd61556609394275cc78895","impliedFormat":1},{"version":"e26c74e599a55bda4bcdb20a9d5caa28728073451944e358f9d5de19c77fe408","impliedFormat":1},{"version":"89d717a0978df19ddbcdc62e43b2972e8c374de52518c2e8ac8a43992b76533a","impliedFormat":1},{"version":"67d4363beaca9c0ba210bbfc33fbef91d1c22c77d5717aca547dd8b650fd0fbc","impliedFormat":1},{"version":"47175f6d1984ee2663d867225c642bc2645d2b37508f0a2b0693cbcd38bf1b8f","impliedFormat":1},{"version":"e5bfab23943623a63a9612d1efd4b9cbae9abe99d88286246fc4ad9564b8078f","impliedFormat":1},{"version":"192d1e66dfb3f57d196fd9db6576d43a8a57af0ec7995eae139bbbc8270427ec","impliedFormat":1},{"version":"ee03033435713344042f5ee1b2f5573f988462c3c6c7dabd4a62df9830bcdd23","impliedFormat":1},{"version":"bfca9c2cc975dd04310c02d200c61baac194b8ebb52d26ea83c50092ae005f88","impliedFormat":1},{"version":"bcd9b17745bc73031b9c23b834dfdc411dec8f6309c5c3ed5dfc05ad9271f2b3","impliedFormat":1},{"version":"a8114e7ae77ce8038227e4b5ffe064b12d2f228e42bf090bbc8a43e4b36774fa","impliedFormat":1},{"version":"123cd45999e04cbd809b33dd14b83a7cdf5bd42ef5419b9dd912f13408fa72be","impliedFormat":1},{"version":"6db08c20642e5c3b7b395d29c53380265aa22a5e9409df7e67b27194f5068369","impliedFormat":1},{"version":"06944bcadb20a137ec5cccefa6d02beb73f5d2889a3dd6409e704acdcd05030f","impliedFormat":1},{"version":"9e21f2c882a1ee090d1d72e7d48b0f4c338ebcc643f684786f8f71ea317d0761","impliedFormat":1},{"version":"4af5e1d11bed0c5594ada90b8169a367bb0622692d904d04e7b0e0fb51b4563a","impliedFormat":1},{"version":"ab428a4142c089f84f277bb4aa7d514f454b69b67a47b9226804178c50ea9b88","impliedFormat":1},{"version":"f2ecf41bea9d32be3e27c71b7d1d9e3f58083b45fd36bd25980899a1168ee109","impliedFormat":1},{"version":"0adcb5db0aa0de1425495b135dacaf6f2240e3ad48b905cd8139f730fedaa4ca","impliedFormat":1},{"version":"116fd7b3ef6684531a1590b36a3ea2d0cd5383230d8509b928e567c08697a3ee","impliedFormat":1},{"version":"ee9d7a75e75cc2a61d2c781df3864e5a0d4f3dbd149ed22c65749bd733375d88","impliedFormat":1},{"version":"35161f13181515b62e1d83bbf6c4062dd64df50b943b11b512bde6e3b4c7f4c9","impliedFormat":1},{"version":"7827df7348614d14296887c07549eab5c11dba6f201519eae67ac811d63a24fb","impliedFormat":1},{"version":"5199dc536c144915831cdf5569baab87db25e7926e82af048b28adc2044f1e5c","impliedFormat":1},{"version":"ea9a572684e0f6bdf35740966b2aaaf341a49ff0eaa0d25f3113f518741d0eff","impliedFormat":1},{"version":"1ee3efb6da370bd447ba467b5edb6f79ecf4082eeda41e201112b4bba48cd9e7","impliedFormat":1},{"version":"f6650f06155f0147cb49d8230ac3d07ac7fc5d38fb5f5db23ca94aebf9a2b240","impliedFormat":1},{"version":"76eb98cfa62a7c06c358c566646ad1bb030b35df08fc20f77f63d733621a9b1d","impliedFormat":1},{"version":"118d80f9540cfa2b110c2835648059d35660b02250e6ac64b7d81791ccb5a5e1","impliedFormat":1},{"version":"5aeb1520ee864150fd128807768ecff927b5b716be83da72869936a8ca723ae7","impliedFormat":1},{"version":"2ad51cec69437b45a5135e5cafbd8e0bbf361e352c65c02ba57ab190e5cf111f","impliedFormat":1},{"version":"375a1e03a1d0c742157b80591ebabab92929be9d3329eac73269a364dcec37be","impliedFormat":1},{"version":"7a91879080f4028bd9a3e76a299f356fa4b1440a36953489c1c0de570529a19b","impliedFormat":1},{"version":"060d93a9be6f0b10b925959577614918a73318293b6f75392987ba5dc32821dd","signature":"17fb5750368eeec430e05d02350094a3d87709ba924d622223eea720c3eadf60"},{"version":"2940a44d9c23110d9dc2fa75a81c5312a9f52116b429b9c0f9d9e5e6f0ff74a9","signature":"d0c08cc38b8d1f405d3972d750f4bd01394345a4f1f549a4bb9e421a480e613c"},{"version":"d95953d2fb0c059266530bf031b958b9ac518c0cb4812e1b0c7baf16a739853f","signature":"d934a3c076ebaef0f56281484d7b88d333cf787ca2e4403192003fe746897b11"},"1affef5e5a5f57615fb044f0a33077838cccb4f8382a311bb037084b0a091f77","8079971ea09988a65bd425cbd4e81b875e0129f39d5e3dc37a6ad982d66d256b","bae24a77dc33ee3a80cbaf2dfbd7f7c6acb6edde4bff39163a3058c93c14e0d2","df2a52a6abb404f784afb39222e74db1074e040bdb1a62813b841d4f9d695438",{"version":"e5259ff908cd94f870b3aca05a2e2b5331da3b4d4b833e0e44a0051ac438b7ee","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"79e16f645be25b58b2aba1a34c0cccf312eee59e99a37a2e1c32ff2f5b5f9fa0","impliedFormat":1},{"version":"2aee4866aaadd28a511b8ccbbf5de09b05d3fef4c3d4e2b925df67bd56379e32","impliedFormat":1},{"version":"567e539302fcf0cff85868ba28b1d377e577b7df2f9bd8f15ff294db6eab8f41","impliedFormat":1},{"version":"092fe093266abdd6a1d66bf0b7a5d6abbdcf7e860dfc0c8e0de6d4e262b147b6","impliedFormat":1},{"version":"d00cd46c963aa004e009458cae42d93d98266f82842f08836701b0abc7eaffa2","impliedFormat":1},{"version":"9d6edb8d1eef6ecbb7d5c031fd810b6fbd20f938ca36d620e1ff6f715ea7bdea","impliedFormat":1},{"version":"bd19825b929ed02095afc0d3353f270ca7ce5143055753174153d59a2cf76da2","impliedFormat":1},{"version":"95c11dddc8f61bfbf8e041e657d3f23d60db75fca921ad12ec86e479783f3734","impliedFormat":1},{"version":"0034b70dbb922bb3eb6102957948969d28a824256b48f5645df2ae7ef0f4fd3e","impliedFormat":1},{"version":"395225adaef9e8588113a04f07b6b387068cd8e7a0230cb059750653808669f2","impliedFormat":1},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"b406a701b85ba51c1e228606dc0e24fbe552df32935965bf34abe698554c0027","signature":"8b1f3922430b4de1ea290dc7219afc7023b5a8f4b5126c0bc2fb67d72239e667"},"ee083b355085d100be23d1decf29d53d4ddefdda6ae215abc7cbbf131735ceef",{"version":"571c9e6d4ec3635ef131f0767347d20e4c7868a2cd3ce56c6fb5f1c15f0f9c35","signature":"99a775f82d53af53fbd4340ea421d16d13698ce4108a268748ca2ba9c4ef5df3"},"4adacdd10660b791915a8c65ed0d10e65b3efe8efcc6adad409ee58408c27e29",{"version":"5d3e73857630bf788ead3d85f3ce9cbffd58501b7ec0df5a091f36dd7a5dff35","signature":"fcfa54765a6fc0a8f6131817215f0c174f2e4cc12ca05cd5a30980f1bd10453e"},{"version":"d51ce83f7eea5362f73585a4ec7ebf6f5774ad87dee6e880e17c0edd8d5d7a11","signature":"1a83d67b08524d4f1719c1e76cd5a6fd1ed58f3104e662dd9c0fd7569b30bdf7"},{"version":"ba82079753d7e454cf3b2a466960f90e2d645f54fb842a5d2bf8582d6398fb59","signature":"6801339821a88f89e3da45a07c2788c796f39159ab4a218a5c5622ec347aac60"},"4854ae5d9afcc0b47241b403a52eb2d9604755cd360d765eee87eebda2cab336",{"version":"b8d099fb63190d0c37a7000a2b8071311a3283e586fab8b716276ad7ca114320","signature":"ba40efe2d4b63d17837dfa911cc0a780c7ff1525a753edb7dd475a44acd3b74a"},"09862b74511910b955a0fb69a1dbb5f8906fe6f194a6079e4d61f8b77dd00ea0","5603469ac3dece937750f0813773a621ee6bbdb157f920b90252edb63efd6709","dd5daf84e0584133c7bcbb7d4952ea2d003d39d06ae3df29903c121004199212","73b47eba1b89d8932c9c1386ececd401508ba7a57c64faea6f702891e8030c59","24e78319c562dbcb524ab159ba98032eb5469d15077c95ec4844eb0c06b8e3d0",{"version":"aefac0e44e7040835309e120bf4e93a5955a3ca552ab5440c2db3d96b1dc90a9","signature":"0946a3931a4f5318dccd7dfe7393b40292a0fd4464b61c6276233c5dd9cbeb9a"},"8e50584d81029a0f06d91becc711ecef78f147a6e6f42397551aff560457f6b1","bd0e4be8ab627f0dfba25c7cafe5917faf502a874bf15e85d438101e7a6d94dd","8b7f1430b8036843e961d0a8e8d404acfb8de20ad9804b5ba249bae7c812d5da","91a09454a027d96924f4f3c3cf6468b33660236f9c2da44221f14dbbfd23df52","3b5660e245775469d77072ab4d9dfc1dd55184b8d883af4d2a9d89c5053855b5",{"version":"91b4ce96f6ad631a0a6920eb0ab928159ff01a439ae0e266ecdc9ea83126a195","impliedFormat":1},{"version":"e3448881d526bfca052d5f9224cc772f61d9fc84d0c52eb7154b13bd4db9d8b2","impliedFormat":1},{"version":"e348f128032c4807ad9359a1fff29fcbc5f551c81be807bfa86db5a45649b7ba","impliedFormat":1},{"version":"42f4d7040a48e5b9c9b20b5f17a04c381676211bdb0b5a580a183cf5908664be","impliedFormat":1},{"version":"d4e4fbb20d20cc5b9f4c85f2357f27cb233cd01f8ca6d85dcca905ec15143e06","impliedFormat":1},{"version":"c2fc483dea0580d1266c1500f17e49a739ca6cfe408691da638ddc211dfffad0","impliedFormat":1},{"version":"dfc8ab0e4a452b8361ccf895ab998bbf27d1f7608fae372ac6aa7f089ef7f68d","impliedFormat":1},{"version":"cca630c92b5382a0677d2dedca95e4e08a0cae660181d6d0dd8fd8bdb104d745","impliedFormat":1},{"version":"2ba3b0d5d868d292abf3e0101500dcbd8812fb7f536c73b581102686fdd621b4","impliedFormat":1},{"version":"c16c3b97930e8fbf05022024f049d51c998dd5eb6509047e1f841777968e85c1","impliedFormat":1},{"version":"cce15e7530c8062dea0666a174f31c1fe445a97357885480748b072778fc6f36","impliedFormat":1},{"version":"535b2fc8c89091c20124fe144699bb4a96d5db4418a1594a9a0a6a863b2195ae","impliedFormat":1},{"version":"dd5165bf834f6e784b4aad9fae6d84307c19f140829e4c6c4123b2d1a707d8bd","impliedFormat":1},{"version":"7ccf260729e19eed74c34046b38b6957bcfe4784d94f76eb830a70fc5d59cb43","impliedFormat":1},{"version":"21575cdeaca6a2c2a0beb8c2ecbc981d9deb95f879f82dc7d6e325fe8737b5ba","impliedFormat":1},{"version":"00343c2c578a0e32ecc384ed779ff39bc7ec6778ef84dc48106b602eb5598a6c","impliedFormat":1},{"version":"c333b496e7676a8b84c720bdece6c34621e3945b7d1710d6ed85d8b742852825","impliedFormat":1},{"version":"3eb7d541136cd8b66020417086e4f481fb1ae0e2b916846d43cbf0b540371954","impliedFormat":1},{"version":"b6fed756be83482969cd037fb707285d46cbb03a19dc576cff8179dc55540727","impliedFormat":1},{"version":"26602933b613e4df3868a6c82e14fffa2393a08531cb333ed27b151923462981","impliedFormat":1},{"version":"8fc19c7114cfd352ff9fb615028e6062cb9fa3cd59c4850bc6c5634b9f57ea27","impliedFormat":1},{"version":"05942150b4d7e0eb991776b1905487ecd94e7299847bb251419c99658363ff84","impliedFormat":1},{"version":"073c43eff28f369a05973364a5c466859867661670eb28e1b6f3dd0654dd0f0e","impliedFormat":1},{"version":"4a7c3274af9c78f7b4328f1e673dec81f48dd75da3bc159780fb4a13238b6684","impliedFormat":1},{"version":"1134991f69fff6f08bd44144518ae14bc294d6076dba8a09574ae918088c5737","impliedFormat":1},{"version":"259a3d89235d858b3d495dc2d1d610d6ce4ac0e91da1ae6a293f250d895d45dd","impliedFormat":1},{"version":"369b7270eeeb37982203b2cb18c7302947b89bf5818c1d3d2e95a0418f02b74e","impliedFormat":1},{"version":"f4c772371ce8ceaab394e1f8af9a6e502f0c02cbf184632dd6e64a00b8aeaf74","impliedFormat":1},{"version":"039bd8d1e0d151570b66e75ee152877fb0e2f42eca43718632ac195e6884be34","impliedFormat":1},{"version":"89fb1e22c3c98cbb86dc3e5949012bdae217f2b5d768a2cc74e1c4b413c25ad2","impliedFormat":1},{"version":"855b9b5ec5cc534fcf9c2799a2c7909b623fcb4ed1a39b51d7c9f6c38adec214","impliedFormat":1},{"version":"58da08d1fe876c79c47dcf88be37c5c3fab55d97b34c8c09a666599a2191208d","impliedFormat":1},{"version":"7e541f14d37444b3f3c3580ed807c36733f9c9137fe7297f46732a459b7e2739","signature":"754cfa02470aa619c32f0bfcd45bd79448307dc2084d91faba186b22614a2bc5"},"ae3da770eeee627cdae660f56b9caddd0461aa044180b71c647d530c8eeb8403","bbab50527f325d4e80133ce651f64ea3dc1f27399b5735c52f08129b78c8bf09","38269b55458b54b8a6d555c68861332f7b77ff117142150ff8826e66603098fa","df40cc3cbe86197d5e37cc4587c577e061e43dfb43ae6e008778bae4335540d6","7f02d3dd69e1ac0b5bc3273cf85bff845aded8c19bf9941fa81543aaf1aa514e","d3fda1b56c7504b55de0ce37d2ad5650281df47a2c9f2ca7cd0f2c579a8f2e43","cbae4ef4a9000af7e553d0a4fc79b191df7c0b9aad7a7db8771a3aa8cb1c00e5","28df56ca2d784df73ac68181c83aeadafd6ded9cdae6847f53f4359e6e9baea2","b2cd408850ad9df8a41aa93085c6fbf47ed5aec4154dc59b8837f8b4d700ef24",{"version":"e4aef6fa7bad1326f8edc066ad796a1634aaec954a97b878d46a7804c73bfdb5","signature":"ce5775cb39c31f310b1abcade60187859a376af581aeb9cb644ce7bd2fc158c8"},"d381452bb935754c0c86bf185f1cecd58800e3cf9147748c8ec80a92fc852e6e","2971e28479774838c8f06bc7fa913924106d45877c58da7f2a4b97d6bd76516d",{"version":"92ec74a116d454015264ec3a1238416c800b352c423b0773deee6f5ed6ddce8b","signature":"4536241a2b6b50c093c96b45b6249d9bf51ad460b8a5f5b67b7e012e0d03c05d"},"5b583a7f8b2fc3c7f29e8aebc73c99cbe8df5f6e59a0c2427f90e89f8bdfb13d","a2eb6498e964b2418d0d178da23c426dbd28eaa8392aab8d7efaacddea9a5b9f","590cdcbea0c9d5bfa62a9b76fc1ad7a2fba8b14a43c08ea8cc3bde29911cbe62","c060799d029416006ea24e10c86c8f1cdf2bb32b6fa2478cb4a941798bbb15f7","616a980aca196d3779b5b483ddba67c2ce2f7ae0a3c344dd13b400de2e601d7e","a3e4d5b357b8059bbe77f1ba4e1b5ac418c887391ac17f752c06fb45750de0e4","b8d3773a936faaccb054f7ab53a3794816556df48fc7ff443749e32eba63b94a",{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"19efad8495a7a6b064483fccd1d2b427403dd84e67819f86d1c6ee3d7abf749c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eef826bc4a19de22155487984e345a34c9cd511dd1170edc7a447cb8231dd4a","affectsGlobalScope":true,"impliedFormat":99},{"version":"6bfdbd285cb25907f31ba9db446e61eb2aa55cf46220ebe5fceaa18b2a0b26e8","affectsGlobalScope":true},"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","9e5985581e4bf2d7d7ebebc5464404da39c4eb552ee07bcbceb669c108352a99","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",{"version":"0b026968c66026a85c16aab5581ca03688da142dbc2bc5afcbaec968a8d27350","signature":"1c57ca6c8cb5921c3246069f61d6479fcb659bb2de7406c009aad991aa1c3b9b"},"c536f8088dfa642e704423872fd8017c3049f6ee2046465ea879494cae0650f1","3ffc7b76098f0c4688a57b9a2948efff274e65571b871f3f6dcc43786a0eddee",{"version":"55aec2fd68e2abfa451013955bd6cac5618f5bc814cb0131d9c3d0e9283dfafa","signature":"8f42eae87de9268f64d1e20cb3359677a1ca99dc0205c993ca00747667674489"},"e3f335a00c1b2165f20e125cb29536b75b4fcc95c1760f2803b95feb2ded15bd",{"version":"73a0ee6395819b063df4b148211985f2e1442945c1a057204cf4cf6281760dc3","affectsGlobalScope":true,"impliedFormat":1},{"version":"d05d8c67116dceafc62e691c47ac89f8f10cf7313cd1b2fb4fe801c2bf1bb1a7","impliedFormat":1},{"version":"3c5bb5207df7095882400323d692957e90ec17323ccff5fd5f29a1ecf3b165d0","impliedFormat":1},{"version":"7959556e748fee75a1ed10822bed4e92edae99ed75e428b650376bacb20ce257","signature":"5a81de12417101e8964d1370944563204a470760be39079613c910b549a0b53a"},"4ca18ab8cd76fb5fb9f7013772f05cdf7b17cdd2f65c39a20399c01c04952a9e",{"version":"a0acca63c9e39580f32a10945df231815f0fe554c074da96ba6564010ffbd2d8","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"3b724a66c071d616203133f8d099a0cb881b0b43fd42e8621e611243c5f30cd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a38efe83ff77c34e0f418a806a01ca3910c02ee7d64212a59d59bca6c2c38fa1","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"3fe4022ba1e738034e38ad9afacbf0f1f16b458ed516326f5bf9e4a31e9be1dc","impliedFormat":1},{"version":"a957197054b074bcdf5555d26286e8461680c7c878040d0f4e2d5509a7524944","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"e9b97d69510658d2f4199b7d384326b7c4053b9e6645f5c19e1c2a54ede427fc","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"f478f6f5902dc144c0d6d7bdc919c5177cac4d17a8ca8653c2daf6d7dc94317f","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"b200675fd112ffef97c166d0341fb33f6e29e9f27660adde7868e95c5bc98beb","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a0a1dda070290b92da5a50113b73ecc4dd6bcbffad66e3c86503d483eafbadcf","impliedFormat":1},{"version":"59dcad36c4549175a25998f6a8b33c1df8e18df9c12ebad1dfb25af13fd4b1ce","impliedFormat":1},{"version":"9ba5b6a30cb7961b68ad4fb18dca148db151c2c23b8d0a260fc18b83399d19d3","impliedFormat":1},{"version":"3f3edb8e44e3b9df3b7ca3219ab539710b6a7f4fe16bd884d441af207e03cd57","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"d71535813e39c23baa113bc4a29a0e187b87d1105ccc8c5a6ebaca38d9a9bff2","impliedFormat":1},{"version":"8cf7e92bdb2862c2d28ba4535c43dc599cfbc0025db5ed9973d9b708dcbe3d98","affectsGlobalScope":true,"impliedFormat":1},{"version":"278e70975bd456bba5874eaee17692355432e8d379b809a97f6af0eee2b702d8","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"636302a00dfd1f9fe6e8e91e4e9350c6518dcc8d51a474e4fc3a9ba07135100b","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"e1ce1d622f1e561f6cdf246372ead3bbc07ce0342024d0e9c7caf3136f712698","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"27e4532aaaa1665d0dd19023321e4dc12a35a741d6b8e1ca3517fcc2544e0efe","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"8c2ad42d5d1a2e8e6112625767f8794d9537f1247907378543106f7ba6c7df90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"98ffdf93dfdd206516971d28e3e473f417a5cfd41172e46b4ce45008f640588e","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"12e8ce658dd17662d82fb0509d2057afc5e6ee30369a2e9e0957eff725b1f11d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74736930d108365d7bbe740c7154706ccfb1b2a3855a897963ab3e5c07ecbf19","impliedFormat":1},{"version":"858f999b3e4a45a4e74766d43030941466460bf8768361d254234d5870480a53","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"63b05afa6121657f25e99e1519596b0826cda026f09372c9100dfe21417f4bd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"3797dd6f4ea3dc15f356f8cdd3128bfa18122213b38a80d6c1f05d8e13cbdad8","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1}],"root":[[157,171],[229,235],[249,268],[301,321],[328,338],342,343],"options":{"allowImportingTsExtensions":true,"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"esModuleInterop":true,"jsx":4,"module":99,"noFallthroughCasesInSwitch":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"strict":true,"target":7,"useDefineForClassFields":true},"referencedMap":[[265,1],[266,2],[267,3],[268,4],[233,1],[235,5],[234,6],[329,7],[330,7],[256,8],[254,9],[255,10],[303,11],[301,12],[302,13],[321,14],[317,15],[318,16],[314,7],[315,17],[312,18],[311,7],[307,19],[310,15],[319,20],[316,21],[309,15],[313,22],[308,15],[320,23],[231,24],[232,25],[230,7],[264,26],[263,9],[331,7],[249,27],[250,28],[251,29],[252,30],[257,7],[258,31],[259,15],[260,32],[306,33],[305,34],[262,35],[261,15],[332,36],[304,37],[333,7],[253,38],[336,39],[335,40],[334,29],[328,41],[337,7],[338,42],[300,43],[299,44],[62,45],[142,46],[140,47],[141,48],[125,49],[93,50],[104,51],[105,52],[106,53],[126,54],[107,55],[128,56],[108,57],[102,58],[111,59],[110,59],[109,60],[123,50],[103,61],[112,62],[117,63],[113,50],[124,7],[127,7],[114,49],[92,64],[94,49],[101,65],[115,66],[121,45],[95,67],[97,67],[120,67],[96,67],[116,49],[98,67],[100,49],[118,49],[119,67],[99,67],[122,59],[78,68],[61,69],[91,70],[65,71],[64,72],[75,45],[68,73],[67,74],[79,75],[80,45],[74,76],[69,45],[71,77],[66,78],[77,79],[70,80],[89,81],[81,82],[73,83],[76,45],[82,84],[90,85],[88,86],[87,86],[86,87],[85,86],[83,86],[84,86],[63,74],[72,88],[57,89],[58,90],[60,91],[59,92],[56,93],[55,45],[389,94],[390,94],[391,95],[350,96],[392,97],[393,98],[394,99],[345,45],[348,100],[346,45],[347,45],[395,101],[396,102],[397,103],[398,104],[399,105],[400,106],[401,106],[403,107],[402,108],[404,109],[405,110],[406,111],[388,112],[349,45],[407,113],[408,114],[409,115],[442,116],[410,117],[411,118],[412,119],[413,120],[414,121],[415,122],[416,123],[417,124],[418,125],[419,126],[420,126],[421,127],[422,45],[423,45],[424,128],[426,129],[425,130],[427,131],[428,132],[429,133],[430,134],[431,135],[432,136],[433,137],[434,138],[435,139],[436,140],[437,141],[438,142],[439,143],[440,144],[441,145],[52,59],[344,59],[48,45],[50,146],[51,59],[248,45],[49,45],[341,147],[340,148],[339,45],[236,59],[247,149],[241,45],[242,59],[240,45],[245,45],[243,150],[244,45],[237,45],[246,45],[239,151],[238,45],[199,152],[226,153],[200,153],[201,153],[188,152],[202,45],[176,152],[203,153],[204,45],[205,152],[206,153],[207,153],[208,45],[209,45],[219,153],[210,153],[187,152],[211,153],[212,45],[213,45],[214,45],[215,153],[189,153],[177,153],[191,153],[192,153],[193,154],[194,153],[195,153],[198,155],[173,45],[227,156],[228,153],[181,153],[185,153],[183,153],[175,45],[216,153],[182,153],[186,45],[217,153],[222,153],[218,153],[179,45],[184,153],[197,153],[220,153],[196,153],[221,153],[190,153],[180,152],[223,153],[178,45],[224,45],[172,45],[225,45],[174,45],[269,45],[284,157],[285,157],[298,158],[286,159],[287,159],[288,160],[282,161],[280,162],[271,45],[275,163],[279,164],[277,165],[283,166],[272,167],[273,168],[274,169],[276,170],[278,171],[281,172],[289,159],[290,159],[291,159],[292,157],[293,159],[294,159],[270,159],[295,45],[297,173],[296,159],[139,174],[130,175],[138,176],[137,176],[129,45],[136,177],[131,178],[132,179],[134,176],[135,180],[133,178],[53,45],[54,45],[46,45],[47,45],[8,45],[9,45],[11,45],[10,45],[2,45],[12,45],[13,45],[14,45],[15,45],[16,45],[17,45],[18,45],[19,45],[3,45],[20,45],[21,45],[4,45],[22,45],[26,45],[23,45],[24,45],[25,45],[27,45],[28,45],[29,45],[5,45],[30,45],[31,45],[32,45],[33,45],[6,45],[37,45],[34,45],[35,45],[36,45],[38,45],[7,45],[39,45],[44,45],[45,45],[40,45],[41,45],[42,45],[43,45],[1,45],[366,181],[376,182],[365,181],[386,183],[357,184],[356,185],[385,186],[379,187],[384,188],[359,189],[373,190],[358,191],[382,192],[354,193],[353,186],[383,194],[355,195],[360,196],[361,45],[364,196],[351,45],[387,197],[377,198],[368,199],[369,200],[371,201],[367,202],[370,203],[380,186],[362,204],[363,205],[372,206],[352,207],[375,198],[374,196],[378,45],[381,208],[327,209],[323,210],[322,45],[324,211],[325,45],[326,212],[156,213],[147,214],[154,215],[149,45],[150,45],[148,216],[151,213],[143,45],[144,45],[155,217],[146,218],[152,45],[153,219],[145,220],[161,221],[162,45],[164,222],[163,45],[342,223],[343,224],[229,45],[159,225],[170,226],[171,227],[158,228],[160,225],[165,229],[168,230],[157,230],[169,231],[166,228],[167,228]],"semanticDiagnosticsPerFile":[[233,[{"start":59,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305}]],[234,[{"start":9,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305}]],[301,[{"start":124,"length":11,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'AccountType'.","category":1,"code":2305},{"start":137,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305},{"start":1728,"length":8,"code":2322,"category":1,"messageText":{"messageText":"Type 'Resolver<{ type: AccountTypeValue; name: string; currency: string; initialBalance: string; description?: string | undefined; }, any, { type: AccountTypeValue; name: string; description: string; currency: string & BRAND<...>; initialBalance: string & BRAND<...>; }>' is not assignable to type 'Resolver'.","category":1,"code":2322,"next":[{"messageText":"Types of parameters 'options' and 'options' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'ResolverOptions' is not assignable to type 'ResolverOptions<{ type: AccountTypeValue; name: string; currency: string; initialBalance: string; description?: string | undefined; }>'.","category":1,"code":2322,"next":[{"messageText":"Types of property 'names' are incompatible.","category":1,"code":2326,"next":[{"messageText":"Type 'string[] | undefined' is not assignable to type '(\"description\" | requiredKeys<{ currency: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>)[] | undefined'.","category":1,"code":2322,"next":[{"messageText":"Type 'string[]' is not assignable to type '(\"description\" | requiredKeys<{ currency: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>)[]'.","category":1,"code":2322,"next":[{"messageText":"Type 'string' is not assignable to type '\"description\" | requiredKeys<{ currency: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>'.","category":1,"code":2322}]}],"canonicalHead":{"code":2322,"messageText":"Type 'ResolverOptions' is not assignable to type 'ResolverOptions<{ type: AccountTypeValue; name: string; currency: string; initialBalance: string; description?: string | undefined; }>'."}}]}]}]}]}},{"start":1822,"length":12,"code":2551,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'AccountDomain'. Did you mean 'currency'?","relatedInformation":[{"file":"../../packages/shared/src/types/account.ts","start":266,"length":8,"messageText":"'currency' is declared here.","category":3,"code":2728}]},{"start":1922,"length":14,"code":2322,"category":1,"messageText":{"messageText":"Type 'number' is not assignable to type 'string & BRAND<\"MoneyString\">'.","category":1,"code":2322,"next":[{"messageText":"Type 'number' is not assignable to type 'string'.","category":1,"code":2322}]}},{"start":2587,"length":16,"code":2345,"category":1,"messageText":{"messageText":"Argument of type '(data: AccountCreateDTO) => void' is not assignable to parameter of type 'SubmitHandler'.","category":1,"code":2345,"next":[{"messageText":"Types of parameters 'data' and 'data' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'TFieldValues' is not assignable to type 'AccountCreateDTO'.","category":1,"code":2322,"next":[{"messageText":"Type 'FieldValues' is missing the following properties from type 'AccountCreateDTO': currency, description, initialBalance, name, type","category":1,"code":2739,"canonicalHead":{"code":2322,"messageText":"Type 'FieldValues' is not assignable to type 'AccountCreateDTO'."}}]}]}]}},{"start":4205,"length":15,"code":2345,"category":1,"messageText":"Argument of type '\"currency_code\"' is not assignable to parameter of type '\"type\" | \"name\" | \"description\" | \"currency\" | \"isSystem\" | \"initialBalance\"'."},{"start":4499,"length":12,"code":2551,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'FieldErrors'. Did you mean 'currency'?"},{"start":4565,"length":12,"code":2551,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'FieldErrors'. Did you mean 'currency'?"}]]],"affectedFilesPendingEmit":[265,266,267,268,233,235,234,329,330,256,254,255,303,301,302,321,317,318,314,315,312,311,307,310,319,316,309,313,308,320,231,232,230,264,263,331,249,250,251,252,257,258,259,260,306,305,262,261,332,304,333,253,336,335,334,337,338,161,162,164,163,342,343,229,159,170,171,158,160,165,168,157,169,166,167],"emitSignatures":[157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,229,230,231,232,233,234,235,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,329,330,331,332,333,334,335,336,337,338,342,343],"version":"5.8.3"} \ No newline at end of file +{"fileNames":["../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/client.d.ts","../../node_modules/.pnpm/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.d.ts","../../node_modules/.pnpm/tiny-warning@1.0.3/node_modules/tiny-warning/src/index.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/types.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/store.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/derived.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/effect.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/scheduler.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/fileroute.d.ts","../../node_modules/.pnpm/@tanstack+history@1.115.0/node_modules/@tanstack/history/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/utils.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/location.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/link.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/routeinfo.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/not-found.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/matches.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/root.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/routerprovider.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/route.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/validators.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/searchparams.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/redirect.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/manifest.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/serializer.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/router.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/defer.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/path.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/qss.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/searchmiddleware.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/structuralsharing.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useroutecontext.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/usesearch.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useparams.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/usenavigate.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useloaderdeps.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useloaderdata.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/scroll-restoration.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/typeprimitives.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/serializer.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/awaited.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/structuralsharing.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useloaderdata.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usematch.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useloaderdeps.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useparams.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usesearch.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useroutecontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/typeprimitives.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/link.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/route.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/catchboundary.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/clientonly.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/fileroute.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/history.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/lazyroutecomponent.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/matches.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/matchcontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/match.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/router.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/routerprovider.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scrollrestoration.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useblocker.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usenavigate.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/routercontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/userouter.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/userouterstate.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/uselocation.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usecangoback.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/utils.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/not-found.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scriptonce.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/asset.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/headcontent.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scripts.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/index.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/scheduler.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/jsx.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/component.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/flow.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/suspense.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/hydration.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/index.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/signal.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/observable.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/array.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/index.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/tanstackrouterdevtools.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/tanstackrouterdevtoolspanel.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/typealiases.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/util.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/zoderror.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/locales/en.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/errors.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/parseutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/enumutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/errorutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/partialutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/standard-schema.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/types.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/external.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/index.d.ts","../../packages/shared/src/constants/pagination.ts","../../packages/shared/src/constants/transactions.ts","../../packages/shared/src/validation/basevalidations.ts","../../packages/shared/src/types/types.ts","../../packages/shared/src/types/account.ts","../../packages/shared/src/types/user.ts","../../packages/shared/src/constants/accounts.ts","../../packages/shared/src/constants/currencies.ts","../../packages/shared/src/constants/operations.ts","../../packages/shared/src/constants/index.ts","../../packages/shared/src/validation/accounts.ts","../../packages/shared/src/validation/transactions.ts","../../packages/shared/src/validation/users.ts","../../packages/shared/src/validation/auth.ts","../../packages/shared/src/validation/index.ts","../../packages/shared/src/types/auth.ts","../../packages/shared/src/types/error-contract.ts","../../packages/shared/src/types/pagination.ts","../../packages/shared/src/types/transaction.ts","../../packages/shared/src/types/index.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/global.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/errors.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/decorator_fills.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/decorators.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/atom.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/comparer.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/modifiers.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/overrideannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/actionannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/flowannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/computedannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/autoannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/generic-abort-signal.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/observable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/computed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/action.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablevalue.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/computedvalue.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/derivation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/globalstate.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/observable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/reaction.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableobject.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablearray.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/spy.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/action.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/autorun.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/become-observed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/configure.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/extendobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/extras.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/flow.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/intercept-read.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/intercept.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/iscomputed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/isobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/object-api.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/observe.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/tojs.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/trace.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/transaction.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/when.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/dynamicobject.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/intercept-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/listen-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/makeobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablemap.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableset.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/legacyobservablearray.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/type-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/eq.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/iterable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/annotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/internal.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/mobx.d.ts","../../packages/shared/src/routes/index.ts","./src/shared/config/config.ts","./src/shared/api/config.ts","./src/shared/api/index.ts","./src/entities/currencies/api/currenciesactions.ts","./src/entities/currencies/model/currencystate.ts","./src/entities/currencies/index.ts","../../node_modules/.pnpm/lucide-react@0.507.0_react@19.1.0/node_modules/lucide-react/dist/lucide-react.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/assertenvironment.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/universalfinalizationregistry.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/observerfinalizationregistry.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/staticrendering.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/observer.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/observercomponent.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/uselocalobservable.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/uselocalstore.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/useasobservablesource.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/observerbatching.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/index.d.ts","../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/clsx.d.mts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/closebutton/closebutton.tsx","./src/shared/ui/closebutton/index.ts","./src/shared/ui/modal/modal.tsx","./src/features/confirmdialog/model/confirmdialogstate.ts","./src/features/confirmdialog/ui/confirmdialog.tsx","./src/features/confirmdialog/index.ts","./src/shared/ui/layout/footer/footer.tsx","./src/shared/ui/layout/footer/index.ts","./src/shared/ui/layout/header/header.tsx","./src/shared/ui/layout/header/index.ts","./src/shared/ui/layout/main/main.tsx","./src/shared/ui/layout/main/index.ts","./src/shared/lib/modalstate/modalstate.ts","./src/shared/lib/modalstate/index.ts","./src/entities/accounts/api/accountactions.ts","./src/entities/accounts/model/accountsstate.ts","./src/entities/accounts/ui/accountslist/accountslist.tsx","./src/entities/accounts/ui/accountslist/index.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/constants.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/createsubject.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/events.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/common.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/eager.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/fieldarray.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/resolvers.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/form.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/utils.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/fields.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/errors.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/validator.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/controller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/controller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/form.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/appenderrors.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/createformcontrol.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usecontroller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usefieldarray.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useform.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useformcontext.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useformstate.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usewatch.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/get.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/set.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/index.d.ts","../../node_modules/.pnpm/@hookform+resolvers@5.0.1_react-hook-form@7.56.4_react@19.1.0_/node_modules/@hookform/resolvers/zod/dist/zod.d.ts","../../node_modules/.pnpm/@hookform+resolvers@5.0.1_react-hook-form@7.56.4_react@19.1.0_/node_modules/@hookform/resolvers/zod/dist/index.d.ts","./src/features/manageaccount/ui/manageaccountform.tsx","./src/features/manageaccount/ui/manageaccountmodal.tsx","./src/features/manageaccount/ui/index.ts","./src/shared/ui/layout/sidebar/sidebar.tsx","./src/shared/ui/layout/layout.tsx","./src/shared/ui/layout/index.ts","./src/routes/__root.tsx","./src/routes/transactions.tsx","./src/routes/budget.tsx","./src/routes/about.tsx","./src/pages/indexpage/indexpage.tsx","./src/pages/indexpage/index.ts","./src/routes/index.tsx","./src/pages/accountspage/accountspage.tsx","./src/pages/accountspage/index.ts","./src/routes/accounts/index.tsx","./src/pages/accountpage/accountpage.tsx","./src/pages/accountpage/index.ts","./src/routes/accounts/$accountid.tsx","./src/routetree.gen.ts","./src/main.tsx","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/hmrpayload.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/customevent.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/hot.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/importglob.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/importmeta.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/client.d.ts","./src/vite-env.d.ts","./src/entities/transactions/api/transactionsactions.ts","./src/entities/transactions/model/transactionsstate.ts","./src/shared/types/index.ts","./src/shared/ui/layout/sidebar/index.ts","./src/shared/ui/modal/index.ts","./src/shared/ui/navbar/navbarconfig.ts","./src/shared/ui/navbar/navbar.tsx","./src/shared/ui/navbar/index.ts","./src/widgets/dashboard/dashboard.tsx","./src/widgets/dashboard/index.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/types.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/index.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/index.d.ts","../../packages/shared/src/libs/formatters.ts","../../packages/shared/src/libs/index.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/index.d.ts"],"fileIdsList":[[51,176,234,237,355,397],[51,176,233,269,270,355,397],[50,51,128,176,241,252,255,271,325,326,355,397],[51,272,355,397],[51,239,355,397],[51,176,233,238,355,397],[51,355,397],[51,259,260,355,397],[51,233,355,397],[51,241,252,255,258,259,355,397],[51,307,355,397],[51,166,171,176,240,252,271,303,305,355,397],[51,252,255,258,261,271,306,355,397],[50,51,52,128,325,326,332,355,397],[51,128,325,326,355,397],[51,322,355,397],[51,319,355,397],[51,316,355,397],[51,128,311,325,326,355,397],[51,128,323,325,326,355,397],[51,128,320,325,326,355,397],[51,128,317,325,326,355,397],[51,128,312,313,314,315,318,321,324,326,355,397],[51,235,355,397],[51,236,355,397],[51,268,355,397],[50,51,253,355,397],[51,254,355,397],[51,241,355,397],[51,256,355,397],[51,262,355,397],[51,264,355,397],[51,310,355,397],[50,51,142,240,261,263,265,267,309,355,397],[51,266,355,397],[51,309,355,397],[51,255,271,273,308,355,397],[50,51,253,257,355,397],[51,340,355,397],[51,128,325,326,339,355,397],[332,355,397],[51,342,355,397],[304,355,397],[156,303,355,397],[355,397],[140,141,355,397],[50,128,139,325,326,355,397],[50,128,325,326,355,397],[91,92,103,109,112,355,397],[50,51,91,92,103,109,112,355,397],[50,51,103,355,397],[50,51,355,397],[91,92,95,96,97,98,99,100,103,109,112,355,397],[51,91,92,103,109,112,355,397],[62,355,397],[53,54,62,91,92,93,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,355,397],[103,355,397],[50,91,92,101,103,109,112,355,397],[50,355,397],[50,51,91,92,94,103,109,112,355,397],[50,91,92,95,96,97,98,99,100,102,103,109,112,355,397],[62,91,92,103,107,109,112,355,397],[50,91,92,103,109,112,355,397],[50,91,103,109,112,355,397],[91,92,98,99,102,103,109,112,355,397],[50,62,91,92,103,107,109,112,355,397],[91,92,94,103,109,112,355,397],[77,355,397],[71,72,355,397],[61,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,355,397],[62,63,64,66,77,107,355,397],[62,72,107,355,397],[63,66,71,77,355,397],[66,77,355,397],[70,71,355,397],[63,65,77,355,397],[61,63,64,65,66,67,68,69,70,72,77,355,397],[61,63,65,71,77,355,397],[60,62,63,64,65,66,67,68,70,71,72,73,74,75,76,78,107,355,397],[64,65,66,77,355,397],[63,64,77,355,397],[63,65,71,355,397],[72,355,397],[63,355,397],[63,65,66,74,77,84,85,355,397],[63,66,77,355,397],[65,77,355,397],[71,355,397],[55,56,355,397],[57,355,397],[55,56,57,58,59,355,397],[56,57,355,397],[55,355,397],[355,394,397],[355,396,397],[397],[355,397,402,432],[355,397,398,403,409,410,417,429,440],[355,397,398,399,409,417],[350,351,352,355,397],[355,397,400,441],[355,397,401,402,410,418],[355,397,402,429,437],[355,397,403,405,409,417],[355,396,397,404],[355,397,405,406],[355,397,409],[355,397,407,409],[355,396,397,409],[355,397,409,410,411,429,440],[355,397,409,410,411,424,429,432],[355,392,397,445],[355,392,397,405,409,412,417,429,440],[355,397,409,410,412,413,417,429,437,440],[355,397,412,414,429,437,440],[353,354,355,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446],[355,397,409,415],[355,397,416,440],[355,397,405,409,417,429],[355,397,418],[355,397,419],[355,396,397,420],[355,394,395,396,397,398,399,400,401,402,403,404,405,406,407,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446],[355,397,422],[355,397,423],[355,397,409,424,425],[355,397,424,426,441,443],[355,397,409,429,430,432],[355,397,431,432],[355,397,429,430],[355,397,432],[355,397,433],[355,394,397,429],[355,397,409,435,436],[355,397,435,436],[355,397,402,417,429,437],[355,397,438],[355,397,417,439],[355,397,412,423,440],[355,397,402,441],[355,397,429,442],[355,397,416,443],[355,397,444],[355,397,402,409,411,420,429,440,443,445],[355,397,429,446],[48,49,355,397],[345,355,397],[344,355,397],[242,244,245,246,247,248,249,250,251,355,397],[233,355,397],[233,243,355,397],[180,232,355,397],[232,355,397],[196,232,355,397],[195,196,201,202,232,355,397],[177,178,179,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,355,397],[50,288,355,397],[288,289,290,293,294,295,296,297,298,299,302,355,397],[288,355,397],[291,292,355,397],[50,286,288,355,397],[283,284,286,355,397],[279,282,284,286,355,397],[283,286,355,397],[50,274,275,276,279,280,281,283,284,285,286,355,397],[276,279,280,281,282,283,284,285,286,287,355,397],[283,355,397],[277,283,284,355,397],[277,278,355,397],[282,284,285,355,397],[282,355,397],[274,279,284,285,355,397],[300,301,355,397],[129,130,135,136,137,138,355,397],[49,355,397],[136,355,397],[129,130,135,355,397],[130,355,397],[130,136,355,397],[131,132,133,134,355,397],[355,364,368,397,440],[355,364,397,429,440],[355,359,397],[355,361,364,397,437,440],[355,397,417,437],[355,397,447],[355,359,397,447],[355,361,364,397,417,440],[355,356,357,360,363,397,409,429,440],[355,364,371,397],[355,356,362,397],[355,364,385,386,397],[355,360,364,397,432,440,447],[355,385,397,447],[355,358,359,397,447],[355,364,397],[355,358,359,360,361,362,363,364,365,366,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,386,387,388,389,390,391,397],[355,364,379,397],[355,364,371,372,397],[355,362,364,372,373,397],[355,363,397],[355,356,359,364,397],[355,364,368,372,373,397],[355,368,397],[355,362,364,367,397,440],[355,356,361,364,371,397],[355,397,429],[355,359,364,385,397,445,447],[331,355,397],[327,355,397],[328,355,397],[329,330,355,397],[155,355,397],[145,146,355,397],[143,144,145,147,148,153,355,397],[144,145,355,397],[154,355,397],[145,355,397],[143,144,145,148,149,150,151,152,355,397],[143,144,155,355,397],[176,355,397],[157,158,163,164,165,355,397],[157,355,397],[346,355,397],[347,355,397],[160,355,397],[156,171,355,397],[160,161,162,172,173,174,175,355,397],[156,159,160,174,355,397],[156,159,355,397],[156,159,166,355,397],[156,355,397],[156,157,158,355,397],[159,167,168,169,170,355,397],[156,158,159,355,397]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"742d4b7b02ffc3ba3c4258a3d196457da2b3fec0125872fd0776c50302a11b9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"03566a51ebc848dec449a4ed69518e9f20caa6ac123fa32676aaaabe64adae8e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"88e9caa9c5d2ba629240b5913842e7c57c5c0315383b8dc9d436ef2b60f1c391","impliedFormat":1},{"version":"b8903e4347ba78328892f8c30b45a8a0071e06c7bae0b0a621035fbf36f75bcb","impliedFormat":99},{"version":"5771ec76e11a5e8303d8945e8050daee051436150d5285dc818c8df3239f45bb","impliedFormat":1},{"version":"7f9ef84dcfff2dbd0c8ed9f4ea18216619527f5b79df115a430114ea5a77c3f6","impliedFormat":99},{"version":"368514ed24d7fa3dbbd3360ee9aac4a1e629969268276e92b86c0f672f3fbbb3","impliedFormat":99},{"version":"c2845147e33e886027684d887ec5ddfccf5bc16bc5fd2ec0f244883d5cee4318","impliedFormat":99},{"version":"ead2432b3eb851e40625006ccba0cb39325fa7f72fdbaedd19825bff05722f8e","impliedFormat":99},{"version":"a1ae11570949b69e4a4e0ee1adcd109f3258df90ec3c5d4345d276e629d38366","impliedFormat":99},{"version":"c7c33f7321ee10856267aaedfd6b61cf249e608a6a37ea7204ec84d48f9a1e4b","impliedFormat":99},{"version":"648db4b8b239ed5b5e8c93618bbe4384d69d659ddf040ad200ee4915aee2157e","impliedFormat":99},{"version":"b39c758625e62b1cf23c7cc38bbe561ba53463237a44291486179bc8c4ec138a","impliedFormat":99},{"version":"33686400541314fa6a0757685b354778628768e2b0ef75310027b2d805cbb3aa","impliedFormat":99},{"version":"c630d607581ce676da4f470e1439fa7127bd763a3608a811122a6a1b33264a61","impliedFormat":99},{"version":"d33c779e38298df4fa486c3190beb16194ca42a41947abcb4fc491e6f9944a8c","impliedFormat":99},{"version":"bdfa3b05b56953affb355a81976e6bfb35961ce63b746d6382ce3412c8e0be10","impliedFormat":99},{"version":"169ef5a18d968eefdcdbad749ded93d1127eb40dd78ce0a0e2ecd9d42eea1a1a","impliedFormat":99},{"version":"55a3b2901bf0b3e5c482ea6ae5e34469d9e7242a30a7bf4783c9ffce3cfc8aa3","impliedFormat":99},{"version":"8a30efabf741de93c2884ffdc23f16602042449e28729eb9874953b3ea8dc705","impliedFormat":99},{"version":"ae2845848a8a72c5e211230fc6bdb1b001056836c4ccbed881441ecece9ab23c","impliedFormat":99},{"version":"dc6b45c8b0599be8ca11a2e7e96745072b9f12850fb0eeec7a70486dc9f99712","impliedFormat":99},{"version":"28c6caf88a757b504f8a36901e4cb7571098f92eaf971aaf8ff77101e47a72ac","impliedFormat":99},{"version":"35b4c072ee1aaddc8352e5a40f6d5c7dc15865ff0681cf312fef258362cedfe4","impliedFormat":99},{"version":"88879ddcc4dd4d80d7580d09db58d64e01bdf93def80c4a3eeeb8886f7c3500c","impliedFormat":99},{"version":"0923187df6538b37c811a365ef1d7efa9f1b23c0116fe9255841548311e7b18d","impliedFormat":99},{"version":"5aef5866733347f6fbdd9391e8c2a3c58002472b57c7188912d995e90274a24c","impliedFormat":99},{"version":"19c9ef1751b20765eeb092adbc9f77bf0f5674ef6083bace3598859e629611d8","affectsGlobalScope":true,"impliedFormat":99},{"version":"dcd742a716cc29789bc177f1b3fbf3c8bc2c164b31104635e3e9f84881bf4c2e","impliedFormat":99},{"version":"22568395a672835b8f58b51097b55d914e0939b71b9a8a54754865b590e9ac29","impliedFormat":99},{"version":"b6783f68397f6edf1c6bed6c2d7c0d89c6d443df1f3a6453ced1d4e184e7014c","impliedFormat":99},{"version":"2ee61832845cf9fd22b581a4cf2bb53794fe4043bd1f1c130a5568e6e3e21368","impliedFormat":99},{"version":"aed243366f9c480cd45623a8f2266c2c8868773cdb13db8f46acb2173cd407f1","impliedFormat":99},{"version":"9c372493f8a4e1c6276f1ba3e6521122b65d9de25d34dc8e43effc97fd64be54","impliedFormat":99},{"version":"d90b155a976d4312c36a3dd442bcd52e9326e599c523faae4cfb8942468f5298","impliedFormat":99},{"version":"13876cda8b941c4bff97ae9ddf89f2a04c8e4ff59bf9141f72f892815bf30244","impliedFormat":99},{"version":"f4cc088c0853a0bc7b5c65fcb53422898cb4a412ba46ed6c62716d465a351922","impliedFormat":99},{"version":"65657868876a64f14656e233a5f2e73d354fb824794f934e9543aa7c3aebf51d","impliedFormat":99},{"version":"b220e3999b3cbae6d40e997f4227bc4006ab04f511e5d4636dfd63badeffd025","impliedFormat":99},{"version":"3dab370414f0c44d2e2e0acfd10fff6a57de0ad770c6c26456f31cbf21204f16","impliedFormat":99},{"version":"9b02e7d7ab9800f9c65547f697875e45ea533fd31da0ce06e52798032d3bf2c2","impliedFormat":99},{"version":"65735c7848f6f0cebe410726ec8f647859df9667ccad5972674646f79527d684","impliedFormat":99},{"version":"fb0b313c8baca67889b6048356af082e02de1df625f0e9f8962f397ea3ed560e","impliedFormat":99},{"version":"57eb2b3764a445ce26575a813ca9b8999c7239f70ae435140ab662be599a60f4","impliedFormat":99},{"version":"91a93ec2ea237ab400834525faed8b2214852ee6b080dfc14de4b52d765248b7","impliedFormat":99},{"version":"32485636a4ae3491437b850a2f8ed523c63d59da3ec48375601458c4e59819e4","impliedFormat":99},{"version":"a61b89063f853e9a1e16a029b67588dde050af09a3c3427097f21e9bd5bdd158","impliedFormat":99},{"version":"048375d7ff42f93ed5a29318ac1642a83c02ae0c2fd658f345d1c46e0c54a67f","impliedFormat":99},{"version":"f7a3ce9cc643a4b47429ec47de60555f6ae6877523a2d3f43dd9ef7b0e7556cb","impliedFormat":99},{"version":"eb2e6827dc100235cc062144f983d5851b13a0531a8965a42e71bd5218355141","impliedFormat":99},{"version":"d2e4f966279a90171e6351b1bf6706d14054a4e5bcc8d46948fed172cfe11371","impliedFormat":99},{"version":"32515c5c844173bf500057ab01e2c246cf914d92cd10d0fd4767b0f9591627ae","impliedFormat":99},{"version":"5271013c661bf776d575ce29b8a18565c4674434c70af21f542dffd15d29581f","impliedFormat":99},{"version":"e14172688f53d888134dd0d2c90fd9f0205179db8ab99430d5b1309e50e0cdb8","impliedFormat":99},{"version":"6946f7e29e3bc5cb0da6df5b664d628fb47593f4dc6bdfb141ad5e7d7a4e777b","impliedFormat":99},{"version":"396fb90417ba85a0b114a2b8f7887d68a208293ab7f29b08ae7b7010d2f4de47","impliedFormat":99},{"version":"b9abf61560dc97324a08cdeecbed287440b70c9821d025d483f8ce0ff6907f98","impliedFormat":99},{"version":"d59755c3b9f1ce4bc1379ba54846eaf464b9f4db9026e3cbcd25603cae7562e2","impliedFormat":99},{"version":"a610f9cf754bf9513b44ecb4c50d79928d2d8289b7ef31c67a89b105b3492702","impliedFormat":99},{"version":"7d318d2247741b3cc384060c60e77295ffa7210b1df48eb3e2d1533017af3182","impliedFormat":99},{"version":"f11d572e430c0ea5e957ec85608629c7433b0ad6e0b1450f3e9d91667625fb58","impliedFormat":99},{"version":"d41befd3d4185f93a5ccc1bb099891415b7157d0ede46ae88a20210d4e772bbc","impliedFormat":99},{"version":"c8bbb9ce1c891efe0e45fd16fb5c4f0c06e9ad60ec1e025bb0b161ef71f04371","impliedFormat":99},{"version":"e9124172677261d101c54c55f9025727c0eed6e5c071db507f977b9cfece01ac","impliedFormat":99},{"version":"556a85bb9982f7f12101fbb9a96262f1d7a4ad6bd5cb50db62153438ccf373b5","impliedFormat":99},{"version":"e5af5fb67c619f0f050cea978ed642da92bbbad4d24b9b0373432e7ed3199431","impliedFormat":99},{"version":"2163deee5fbd66077b4fbd4d9cf84c8f06918113eff425a62dc257b051eccd02","impliedFormat":99},{"version":"e8ef79b944df8cbaeed8bcf01062709c91853ea06474571748f65e3a03439656","affectsGlobalScope":true,"impliedFormat":99},{"version":"16db9485a97fb736ec9098a6d1277e7b95fc489c8e8608d83824728f47219d94","impliedFormat":99},{"version":"9e1ad280ec936e7789a3901f29b6ec2b68578ca030ef7c4b51e970d6308aa6e2","impliedFormat":99},{"version":"81f50c2e9a72a15dcfd77a3aff3b676bbf2f270265c3e5ab8d64ba07575417ee","impliedFormat":99},{"version":"038780a3ac1630c2398d24aa98283d23216feebc22a265caa5f747d85b9d0b68","impliedFormat":99},{"version":"3562836bbc8f85daa391c5bac66cc6c85be806b7d1a0bf25bc6897481e555868","impliedFormat":99},{"version":"5dda66d705930bbea2e79e0f0a5b74c5642481c73487e9f48b6e6e2a7535321c","impliedFormat":99},{"version":"fcb569cff31c9ffc80018fe31585a7733d6297977b6d5303a7b0c9c87856e41b","impliedFormat":99},{"version":"616e09e0cc203f41e18848c4e246d248d94518b080e2b16cf28bc705fb9b35b5","impliedFormat":99},{"version":"518dbebaadc0fc21aa92177d30b043613471869c7fc16502b170c184e60fc17a","impliedFormat":99},{"version":"027b6effcb25477fb182104e60037753a7861782db46001ae54d50b574d0f74b","impliedFormat":99},{"version":"4cad4e814e224a5f6cc40c0b5dfd3c60f6715147a94c27568324439325578c35","impliedFormat":99},{"version":"a6d2eb77f390448a229ca63b4adadbcc549d203b8c038f1f7627a118f732969d","impliedFormat":99},{"version":"1ddd07729c458dd68c47ea2ac1f46f33f64f603f53fe43d74c9bcd65487923fd","impliedFormat":99},{"version":"b886cc0d907b31ae7484a85da375bc8ac42f9986ec1e49c9c7c854956b0a928b","impliedFormat":99},{"version":"69029230a28323de141ad3b08350b45468248d1f8cce9fe67e4c9aed7785fb2d","impliedFormat":99},{"version":"8ab288ca80e5f2a9dab19955fcd9e81a244432cd7d256fdb5ea198ed5a44f0a4","impliedFormat":99},{"version":"0caadd121b8965f24d5c647a2ed01f5fc352772e4cea4bbca2ab78adedfdd921","impliedFormat":99},{"version":"b642b6a4b9b670338bc50281a6dd192fb7cc0b2f9d63d38c250bb1f02e828bbc","impliedFormat":99},{"version":"3c52dce9553224361c062550a63e7ea0c60a26352e87e80ce847134ae2e4d782","impliedFormat":99},{"version":"c2e65a549ac9d227a9002d077af67bb1c77557a72bf39a85c4354831879078ed","affectsGlobalScope":true,"impliedFormat":99},{"version":"38f11ad59dfc38750b789f9f250742d0ce0da44e28f790a032cd15665b70f239","impliedFormat":99},{"version":"c86b1f5777f36dd334a630de065b339f04e4ba672ade417039740847c11246a5","affectsGlobalScope":true,"impliedFormat":99},{"version":"38d2308df413ce823f4764af63dc7e9aa64223f6ba71dbbd5559a6695ca06a08","impliedFormat":99},{"version":"01b1fb6368b7f9128e594c8ad67cbfbfeaed998ba9a59091764f37dc13c867d1","impliedFormat":99},{"version":"a5908d37bbe4df084fa9527e9d672f5d69859684c722e465ca18a6b11f666d7b","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"950f2cd81e30d0ecdf70ab78fcfd85fc5bb28b45ebb08c860daff059feea412e","impliedFormat":1},{"version":"3a5af4fba7b27b815bb40f52715aedebaa4b371da3e5a664e7e0798c9b638825","impliedFormat":1},{"version":"8485b6da53ec35637d072e516631d25dae53984500de70a6989058f24354666f","impliedFormat":1},{"version":"ebe80346928736532e4a822154eb77f57ef3389dbe2b3ba4e571366a15448ef2","impliedFormat":1},{"version":"49c632082dc8a916353288d3d8b2dc82b3471794249a381d090d960c8ceac908","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"71addb585c2db7b8e53dc1b0bcfa58c6c67c6e4fa2b968942046749d66f82e7e","impliedFormat":1},{"version":"c76b0c5727302341d0bdfa2cc2cee4b19ff185b554edb6e8543f0661d8487116","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"e703cfacb9965c4d4155346c65a0091ecded90ea98874ed6b3f36286577c4dde","impliedFormat":1},{"version":"f5ef066942e4f0bd98200aa6a6694b831e73200c9b3ade77ad0aa2409e8fe1b1","impliedFormat":1},{"version":"b9e99cd94f4166a245f5158f7286c05406e2a4c694619bceb7a4f3519d1d768e","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"9563572d81b4aa8b7cf3e24384262341bd53a7a1e5e55034f5d446654b824e8a","signature":"bce0cc5be1903519d3a629f49137b213c22e1d4380a6b4cd409fe5bc38a7dd09"},{"version":"10e44a443fcc02629276b3068a04090ef67456c7b9f49a767e566575625852b7","signature":"2966ffb8b17167488deb293aaa49f5da898066d32ce422afbbf98fc516a42fc1"},{"version":"4d2f510ca3eb24a383eeb142531bf976ffd0e0ea31d960f9b16bc333cda03685","signature":"fc1d8114ef6048cea25504969c001f14e49fc0c90afcce05bc0b465028cdaf71"},{"version":"1fdf5e9f472a68188429b3ba3332c46a9abf74de0e23a43f2f1b4f6579275a9f","signature":"46079b82d154f2c3ae8242a4ef906257a544e05934be47ba373ec035c0248143"},{"version":"9d48cf5ea6f767f9fb3b7a86e9406946936dbbdb1d32c4b9e5c2892928a70db8","signature":"ad0af5ae3b32b4d2e50b0a1fd4b8f38be5da7509962c47bebe76a73ac056b188"},{"version":"5c043033f251a8c5bd7108914393af575650fa9c78a78e548f81d7cfc4014e6f","signature":"74555895068502b65799c3c9928e42f4e56d6f69b021e6f13f294c1a8c354260"},{"version":"b9b5488f9e8043ff017f648745a75b1c2e030a8efa16c6310fa6e6f9e52bcef5","signature":"58969267c23c27443642dec465082e37d7e210fa71c2cf13f516d876ff97b6fa"},{"version":"5275b12537eadeeac78e852fd6ddebb4882770947c64f06bf1bdaf29ecb61303","signature":"c4a90e38375dfd22ab91d9a420cfa703a5dca4145108e0d1106e9873bc96797b"},{"version":"1af38648bd7056b360f5c3607b305f558a09d1d320fa253a7333c169ea9529b1","signature":"08f9b11447950c4f59141f328520c6e31b5a166924336777995a0187f7a4bde9"},"9b1f5337bffec91b0d8559a08d004f2f35357247a6581f8e0650200f344aa1de",{"version":"09ff1fb2ef8f9fa73f08613aa64167219bd12fa732fd9d7bebfe0edabd408d04","signature":"8fad78010d87e993a6a2e50b595266879bc0bfad0209607b0974f0425f71c9af"},{"version":"58483c4909f03f36a319216982788a7d91ca3cf2dd2a341b627136428ae85793","signature":"9d202fb756378d3f79af158bccccd4c6e9b2c525c7e5b6f88bdd4c23c78cfc91"},{"version":"b065b977696599b488831c0189811f198445b31d2684e1017c0dd5a486d7c3ac","signature":"a6785ff8e286cc4b64221c4d9c6a1084f328837d1e3225db9045130dcc8a0ca8"},{"version":"c4a595c3f6c279163c73fde607c056c813ef53f47537bc91b2623dd95f45f2fc","signature":"0dacf73b071d41f5506c1858dbc30a73b85482307cb5e15a63ba4fe654906ce4"},"bb0b00be806f71a00f4a806b9331cc7b919d652d35a53ed9e2774b017dcf1dbd",{"version":"3b96187464c836feb9457f746b1ed44dc91ad76aa65ee8cdd64283b46fda9234","signature":"ec7adc98a93755b7c1ecb861e78423a549a0d8b30751571340bac9c0c974c66a"},{"version":"b4b0f493c3ebf51fc982a1ce59e95b863836dc00c5685d09289cf1953a40fba0","signature":"db39cdaf4c7f6939a6e0533c49464ad9d9a7699adb355247500f92cce14d2a67"},{"version":"b4508ea58d02c3b0931995c9d5a5f464864efa7242243677de743ff9d248ab0d","signature":"02b514f6c19f6e26cd1993dd2504a4d94566ea6af541e90dfcc8ed378c1f0eac"},{"version":"06d0dba413a4d4846e0a93f03e50064f47185987ea23efb769aa14cb22e3e0c8","signature":"83df49fb43b8081fd8c4bcf9ef85f5869b79427dc76a2ac650a94959a1b6a93b"},"5177d8c52d115cda5ab89dcaecbe648a34699b662cecec12d17ba39485b80b92",{"version":"8c72ef1bb77e1dbfe10f37d22327de08b839246785253756944b94c8a328eece","impliedFormat":1},{"version":"26efb34ac3797956919cecf67aa2c229b875620bb7a4eb16e3ab97e2fafe0c76","impliedFormat":1},{"version":"4b90c2e95674469fb79ad5c7f2fd946304acc2910bf578f7b1929fead92a1d27","impliedFormat":1},{"version":"6ba3836f4607d0e8a2da4d7a47cd9a5750703d69dd07281997c39831a2edbade","impliedFormat":1},{"version":"56024065be958aac94c30535a04a6bd8db908b2924a21011009291353fcd2478","impliedFormat":1},{"version":"ac339e7b2a6953534dc34b2c76b5128c84eb33b947e9c14b0c6f819e8974842f","impliedFormat":1},{"version":"33c337de15324843b658d00995db674f9c618fe9ad6ad6b43f0fe6835984fa0b","impliedFormat":1},{"version":"27254b3eb6ba4c0630413a63892534ecbac47746bed81f72d996dca1662936d4","impliedFormat":1},{"version":"7e687be116d78dd903a427dacc3cbad3ff9f7ace60dffa8c83b7102ded92de47","impliedFormat":1},{"version":"3da682a8d8b2cf020828199e06d40f4b8f617d8582ab466354b804602e492b60","impliedFormat":1},{"version":"cca10549035d257a89fc46649190b96534af65715aa85b57f0d2a5c61fb98938","impliedFormat":1},{"version":"5e04be59bf10e4d8adf988610f7e80864f33e7e6dd5072ce4af354305581dec9","impliedFormat":1},{"version":"6cf6a4290ee17a9104da299a794d39fb02ef19e054ab14f2ec1d2753514f131b","impliedFormat":1},{"version":"06648b76a5d56f77a1443626ced0fe0fa0dc58827df8ad6195e331915a46e347","impliedFormat":1},{"version":"abd934f6207c5954c02630d92160c4d866f0664047dae0a939f9bdcdb8302e98","impliedFormat":1},{"version":"66c15e0c870a2928dc58a7b19762e94a32ff4ebe4a64b42e9419d212e51cf2c8","impliedFormat":1},{"version":"e5d037491332f872acd558856a11b23c4d5bf726ca3cb9208c7ab8051c4b0faa","impliedFormat":1},{"version":"157227cf6b14eee7ee92a96862902be7d64a6bda2befecf6ebf80e97079e1545","impliedFormat":1},{"version":"a4bd311189ce445fa39d86b23905683a6cd6d9eb884bf8a100329b3dbfb1db43","impliedFormat":1},{"version":"f4d06ad1b70434339a4fdf464ac4d804154c71314c3e2fb928bead12b620cf1a","impliedFormat":1},{"version":"c3878889468470079a7a3f91ab157b2c15da1191d9253a3149eedf4b7c7e9712","impliedFormat":1},{"version":"b2ef09a1454cd38bec48a17316abe8c6b1d56711b1071e5974ed5c377741131b","impliedFormat":1},{"version":"56290b22f633dc5afd255ed0e897d1e8e6567fa5abf38773c16ba7b89de4c56a","impliedFormat":1},{"version":"698b7a228b992346acdd678b918804f4e68824fbcb5bfa551603aa6d4217548d","impliedFormat":1},{"version":"dd1a80df85e075e40a7edd6d9a549a2a954956bfbdd61556609394275cc78895","impliedFormat":1},{"version":"e26c74e599a55bda4bcdb20a9d5caa28728073451944e358f9d5de19c77fe408","impliedFormat":1},{"version":"89d717a0978df19ddbcdc62e43b2972e8c374de52518c2e8ac8a43992b76533a","impliedFormat":1},{"version":"67d4363beaca9c0ba210bbfc33fbef91d1c22c77d5717aca547dd8b650fd0fbc","impliedFormat":1},{"version":"47175f6d1984ee2663d867225c642bc2645d2b37508f0a2b0693cbcd38bf1b8f","impliedFormat":1},{"version":"e5bfab23943623a63a9612d1efd4b9cbae9abe99d88286246fc4ad9564b8078f","impliedFormat":1},{"version":"192d1e66dfb3f57d196fd9db6576d43a8a57af0ec7995eae139bbbc8270427ec","impliedFormat":1},{"version":"ee03033435713344042f5ee1b2f5573f988462c3c6c7dabd4a62df9830bcdd23","impliedFormat":1},{"version":"bfca9c2cc975dd04310c02d200c61baac194b8ebb52d26ea83c50092ae005f88","impliedFormat":1},{"version":"bcd9b17745bc73031b9c23b834dfdc411dec8f6309c5c3ed5dfc05ad9271f2b3","impliedFormat":1},{"version":"a8114e7ae77ce8038227e4b5ffe064b12d2f228e42bf090bbc8a43e4b36774fa","impliedFormat":1},{"version":"123cd45999e04cbd809b33dd14b83a7cdf5bd42ef5419b9dd912f13408fa72be","impliedFormat":1},{"version":"6db08c20642e5c3b7b395d29c53380265aa22a5e9409df7e67b27194f5068369","impliedFormat":1},{"version":"06944bcadb20a137ec5cccefa6d02beb73f5d2889a3dd6409e704acdcd05030f","impliedFormat":1},{"version":"9e21f2c882a1ee090d1d72e7d48b0f4c338ebcc643f684786f8f71ea317d0761","impliedFormat":1},{"version":"4af5e1d11bed0c5594ada90b8169a367bb0622692d904d04e7b0e0fb51b4563a","impliedFormat":1},{"version":"ab428a4142c089f84f277bb4aa7d514f454b69b67a47b9226804178c50ea9b88","impliedFormat":1},{"version":"f2ecf41bea9d32be3e27c71b7d1d9e3f58083b45fd36bd25980899a1168ee109","impliedFormat":1},{"version":"0adcb5db0aa0de1425495b135dacaf6f2240e3ad48b905cd8139f730fedaa4ca","impliedFormat":1},{"version":"116fd7b3ef6684531a1590b36a3ea2d0cd5383230d8509b928e567c08697a3ee","impliedFormat":1},{"version":"ee9d7a75e75cc2a61d2c781df3864e5a0d4f3dbd149ed22c65749bd733375d88","impliedFormat":1},{"version":"35161f13181515b62e1d83bbf6c4062dd64df50b943b11b512bde6e3b4c7f4c9","impliedFormat":1},{"version":"7827df7348614d14296887c07549eab5c11dba6f201519eae67ac811d63a24fb","impliedFormat":1},{"version":"5199dc536c144915831cdf5569baab87db25e7926e82af048b28adc2044f1e5c","impliedFormat":1},{"version":"ea9a572684e0f6bdf35740966b2aaaf341a49ff0eaa0d25f3113f518741d0eff","impliedFormat":1},{"version":"1ee3efb6da370bd447ba467b5edb6f79ecf4082eeda41e201112b4bba48cd9e7","impliedFormat":1},{"version":"f6650f06155f0147cb49d8230ac3d07ac7fc5d38fb5f5db23ca94aebf9a2b240","impliedFormat":1},{"version":"76eb98cfa62a7c06c358c566646ad1bb030b35df08fc20f77f63d733621a9b1d","impliedFormat":1},{"version":"118d80f9540cfa2b110c2835648059d35660b02250e6ac64b7d81791ccb5a5e1","impliedFormat":1},{"version":"5aeb1520ee864150fd128807768ecff927b5b716be83da72869936a8ca723ae7","impliedFormat":1},{"version":"2ad51cec69437b45a5135e5cafbd8e0bbf361e352c65c02ba57ab190e5cf111f","impliedFormat":1},{"version":"375a1e03a1d0c742157b80591ebabab92929be9d3329eac73269a364dcec37be","impliedFormat":1},{"version":"7a91879080f4028bd9a3e76a299f356fa4b1440a36953489c1c0de570529a19b","impliedFormat":1},{"version":"4a6c26f345674f06453339314f10cc9c2e0b97ef789682d743327fb7b9ea2838","signature":"351477ac802666545603bfba7e9ceff76f00da4a52427c878cff1e6c881df4b5"},{"version":"2940a44d9c23110d9dc2fa75a81c5312a9f52116b429b9c0f9d9e5e6f0ff74a9","signature":"d0c08cc38b8d1f405d3972d750f4bd01394345a4f1f549a4bb9e421a480e613c"},{"version":"d95953d2fb0c059266530bf031b958b9ac518c0cb4812e1b0c7baf16a739853f","signature":"d934a3c076ebaef0f56281484d7b88d333cf787ca2e4403192003fe746897b11"},"1affef5e5a5f57615fb044f0a33077838cccb4f8382a311bb037084b0a091f77",{"version":"8079971ea09988a65bd425cbd4e81b875e0129f39d5e3dc37a6ad982d66d256b","signature":"9b3a62e9c0d0a9bb8f4f3960f68fcc871e793241b177793eb1b74fdf8eaca25f"},{"version":"bae24a77dc33ee3a80cbaf2dfbd7f7c6acb6edde4bff39163a3058c93c14e0d2","signature":"f28363132526035687a0b6b8424d02f7bcc4b73f83b3ab12d6fa9adc51f9b814"},"df2a52a6abb404f784afb39222e74db1074e040bdb1a62813b841d4f9d695438",{"version":"e5259ff908cd94f870b3aca05a2e2b5331da3b4d4b833e0e44a0051ac438b7ee","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"79e16f645be25b58b2aba1a34c0cccf312eee59e99a37a2e1c32ff2f5b5f9fa0","impliedFormat":1},{"version":"2aee4866aaadd28a511b8ccbbf5de09b05d3fef4c3d4e2b925df67bd56379e32","impliedFormat":1},{"version":"567e539302fcf0cff85868ba28b1d377e577b7df2f9bd8f15ff294db6eab8f41","impliedFormat":1},{"version":"092fe093266abdd6a1d66bf0b7a5d6abbdcf7e860dfc0c8e0de6d4e262b147b6","impliedFormat":1},{"version":"d00cd46c963aa004e009458cae42d93d98266f82842f08836701b0abc7eaffa2","impliedFormat":1},{"version":"9d6edb8d1eef6ecbb7d5c031fd810b6fbd20f938ca36d620e1ff6f715ea7bdea","impliedFormat":1},{"version":"bd19825b929ed02095afc0d3353f270ca7ce5143055753174153d59a2cf76da2","impliedFormat":1},{"version":"95c11dddc8f61bfbf8e041e657d3f23d60db75fca921ad12ec86e479783f3734","impliedFormat":1},{"version":"0034b70dbb922bb3eb6102957948969d28a824256b48f5645df2ae7ef0f4fd3e","impliedFormat":1},{"version":"395225adaef9e8588113a04f07b6b387068cd8e7a0230cb059750653808669f2","impliedFormat":1},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"b406a701b85ba51c1e228606dc0e24fbe552df32935965bf34abe698554c0027","signature":"8b1f3922430b4de1ea290dc7219afc7023b5a8f4b5126c0bc2fb67d72239e667"},"ee083b355085d100be23d1decf29d53d4ddefdda6ae215abc7cbbf131735ceef",{"version":"571c9e6d4ec3635ef131f0767347d20e4c7868a2cd3ce56c6fb5f1c15f0f9c35","signature":"99a775f82d53af53fbd4340ea421d16d13698ce4108a268748ca2ba9c4ef5df3"},"4adacdd10660b791915a8c65ed0d10e65b3efe8efcc6adad409ee58408c27e29",{"version":"5d3e73857630bf788ead3d85f3ce9cbffd58501b7ec0df5a091f36dd7a5dff35","signature":"fcfa54765a6fc0a8f6131817215f0c174f2e4cc12ca05cd5a30980f1bd10453e"},{"version":"d51ce83f7eea5362f73585a4ec7ebf6f5774ad87dee6e880e17c0edd8d5d7a11","signature":"1a83d67b08524d4f1719c1e76cd5a6fd1ed58f3104e662dd9c0fd7569b30bdf7"},{"version":"ba82079753d7e454cf3b2a466960f90e2d645f54fb842a5d2bf8582d6398fb59","signature":"6801339821a88f89e3da45a07c2788c796f39159ab4a218a5c5622ec347aac60"},"4854ae5d9afcc0b47241b403a52eb2d9604755cd360d765eee87eebda2cab336",{"version":"b8d099fb63190d0c37a7000a2b8071311a3283e586fab8b716276ad7ca114320","signature":"ba40efe2d4b63d17837dfa911cc0a780c7ff1525a753edb7dd475a44acd3b74a"},"09862b74511910b955a0fb69a1dbb5f8906fe6f194a6079e4d61f8b77dd00ea0",{"version":"5603469ac3dece937750f0813773a621ee6bbdb157f920b90252edb63efd6709","signature":"42e11e0395b48c7f0fdcc839b3556e61da7350e863a2b2d38016a2ced0d42c2a"},"dd5daf84e0584133c7bcbb7d4952ea2d003d39d06ae3df29903c121004199212",{"version":"73b47eba1b89d8932c9c1386ececd401508ba7a57c64faea6f702891e8030c59","signature":"1b16fad6ccf0d677dba98f79ca9c082d61b667e7fecf5a2569b0bd73cbf6e7d0"},"24e78319c562dbcb524ab159ba98032eb5469d15077c95ec4844eb0c06b8e3d0",{"version":"aefac0e44e7040835309e120bf4e93a5955a3ca552ab5440c2db3d96b1dc90a9","signature":"0946a3931a4f5318dccd7dfe7393b40292a0fd4464b61c6276233c5dd9cbeb9a"},"8e50584d81029a0f06d91becc711ecef78f147a6e6f42397551aff560457f6b1",{"version":"bd0e4be8ab627f0dfba25c7cafe5917faf502a874bf15e85d438101e7a6d94dd","signature":"dff0fd676ac8ae89e2135bea132e049e613fd47eea581075f82465845eedbb96"},{"version":"8b7f1430b8036843e961d0a8e8d404acfb8de20ad9804b5ba249bae7c812d5da","signature":"4333bc1154e167925ee4b7ae1d09031b02be4b33ed88fe56180e680e8886cbbf"},{"version":"91a09454a027d96924f4f3c3cf6468b33660236f9c2da44221f14dbbfd23df52","signature":"8216aee5d81ec63248ba300134bb4a1901b789f891940bc9ff8ad00890360c26"},"3b5660e245775469d77072ab4d9dfc1dd55184b8d883af4d2a9d89c5053855b5",{"version":"91b4ce96f6ad631a0a6920eb0ab928159ff01a439ae0e266ecdc9ea83126a195","impliedFormat":1},{"version":"e3448881d526bfca052d5f9224cc772f61d9fc84d0c52eb7154b13bd4db9d8b2","impliedFormat":1},{"version":"e348f128032c4807ad9359a1fff29fcbc5f551c81be807bfa86db5a45649b7ba","impliedFormat":1},{"version":"42f4d7040a48e5b9c9b20b5f17a04c381676211bdb0b5a580a183cf5908664be","impliedFormat":1},{"version":"d4e4fbb20d20cc5b9f4c85f2357f27cb233cd01f8ca6d85dcca905ec15143e06","impliedFormat":1},{"version":"c2fc483dea0580d1266c1500f17e49a739ca6cfe408691da638ddc211dfffad0","impliedFormat":1},{"version":"dfc8ab0e4a452b8361ccf895ab998bbf27d1f7608fae372ac6aa7f089ef7f68d","impliedFormat":1},{"version":"cca630c92b5382a0677d2dedca95e4e08a0cae660181d6d0dd8fd8bdb104d745","impliedFormat":1},{"version":"2ba3b0d5d868d292abf3e0101500dcbd8812fb7f536c73b581102686fdd621b4","impliedFormat":1},{"version":"c16c3b97930e8fbf05022024f049d51c998dd5eb6509047e1f841777968e85c1","impliedFormat":1},{"version":"cce15e7530c8062dea0666a174f31c1fe445a97357885480748b072778fc6f36","impliedFormat":1},{"version":"535b2fc8c89091c20124fe144699bb4a96d5db4418a1594a9a0a6a863b2195ae","impliedFormat":1},{"version":"dd5165bf834f6e784b4aad9fae6d84307c19f140829e4c6c4123b2d1a707d8bd","impliedFormat":1},{"version":"7ccf260729e19eed74c34046b38b6957bcfe4784d94f76eb830a70fc5d59cb43","impliedFormat":1},{"version":"21575cdeaca6a2c2a0beb8c2ecbc981d9deb95f879f82dc7d6e325fe8737b5ba","impliedFormat":1},{"version":"00343c2c578a0e32ecc384ed779ff39bc7ec6778ef84dc48106b602eb5598a6c","impliedFormat":1},{"version":"c333b496e7676a8b84c720bdece6c34621e3945b7d1710d6ed85d8b742852825","impliedFormat":1},{"version":"3eb7d541136cd8b66020417086e4f481fb1ae0e2b916846d43cbf0b540371954","impliedFormat":1},{"version":"b6fed756be83482969cd037fb707285d46cbb03a19dc576cff8179dc55540727","impliedFormat":1},{"version":"26602933b613e4df3868a6c82e14fffa2393a08531cb333ed27b151923462981","impliedFormat":1},{"version":"8fc19c7114cfd352ff9fb615028e6062cb9fa3cd59c4850bc6c5634b9f57ea27","impliedFormat":1},{"version":"05942150b4d7e0eb991776b1905487ecd94e7299847bb251419c99658363ff84","impliedFormat":1},{"version":"073c43eff28f369a05973364a5c466859867661670eb28e1b6f3dd0654dd0f0e","impliedFormat":1},{"version":"4a7c3274af9c78f7b4328f1e673dec81f48dd75da3bc159780fb4a13238b6684","impliedFormat":1},{"version":"1134991f69fff6f08bd44144518ae14bc294d6076dba8a09574ae918088c5737","impliedFormat":1},{"version":"259a3d89235d858b3d495dc2d1d610d6ce4ac0e91da1ae6a293f250d895d45dd","impliedFormat":1},{"version":"369b7270eeeb37982203b2cb18c7302947b89bf5818c1d3d2e95a0418f02b74e","impliedFormat":1},{"version":"f4c772371ce8ceaab394e1f8af9a6e502f0c02cbf184632dd6e64a00b8aeaf74","impliedFormat":1},{"version":"039bd8d1e0d151570b66e75ee152877fb0e2f42eca43718632ac195e6884be34","impliedFormat":1},{"version":"89fb1e22c3c98cbb86dc3e5949012bdae217f2b5d768a2cc74e1c4b413c25ad2","impliedFormat":1},{"version":"855b9b5ec5cc534fcf9c2799a2c7909b623fcb4ed1a39b51d7c9f6c38adec214","impliedFormat":1},{"version":"58da08d1fe876c79c47dcf88be37c5c3fab55d97b34c8c09a666599a2191208d","impliedFormat":1},{"version":"7e541f14d37444b3f3c3580ed807c36733f9c9137fe7297f46732a459b7e2739","signature":"754cfa02470aa619c32f0bfcd45bd79448307dc2084d91faba186b22614a2bc5"},{"version":"ae3da770eeee627cdae660f56b9caddd0461aa044180b71c647d530c8eeb8403","signature":"d6de841d6c0b6cdbe1fb3bab69c5a822db427a38112c3bbed97bcdbf133d5791"},"bbab50527f325d4e80133ce651f64ea3dc1f27399b5735c52f08129b78c8bf09",{"version":"38269b55458b54b8a6d555c68861332f7b77ff117142150ff8826e66603098fa","signature":"bfc4ca2b4e87103961ef1d70f04980c21a27262a66a6551491b0ced2fcfa9102"},{"version":"df40cc3cbe86197d5e37cc4587c577e061e43dfb43ae6e008778bae4335540d6","signature":"24407ccc87811662c18036c5abf7cee2ced3b225c74349062164099d98a8a819"},"7f02d3dd69e1ac0b5bc3273cf85bff845aded8c19bf9941fa81543aaf1aa514e",{"version":"d3fda1b56c7504b55de0ce37d2ad5650281df47a2c9f2ca7cd0f2c579a8f2e43","signature":"337eff7966a27d188fb47faa58b738791db31e69755c4f3edb74424bd58c4e8a"},{"version":"cbae4ef4a9000af7e553d0a4fc79b191df7c0b9aad7a7db8771a3aa8cb1c00e5","signature":"1b68b44bf416d5ac5f36c59a998dab6f33efd46b8812d1c46588176e4454601a"},{"version":"28df56ca2d784df73ac68181c83aeadafd6ded9cdae6847f53f4359e6e9baea2","signature":"1e40c707386547b85eaed90f0a540905e784effbafa767e18435c270e9e3d097"},{"version":"b2cd408850ad9df8a41aa93085c6fbf47ed5aec4154dc59b8837f8b4d700ef24","signature":"a40544dac75a719ea82fe76ec1002be6dce9d2f7eef8d867c2bed8eba533c40c"},{"version":"e4aef6fa7bad1326f8edc066ad796a1634aaec954a97b878d46a7804c73bfdb5","signature":"ce5775cb39c31f310b1abcade60187859a376af581aeb9cb644ce7bd2fc158c8"},"d381452bb935754c0c86bf185f1cecd58800e3cf9147748c8ec80a92fc852e6e",{"version":"2971e28479774838c8f06bc7fa913924106d45877c58da7f2a4b97d6bd76516d","signature":"56055c89baec056fa1aa797807cf5b6f9bd3a0f322d4101d7cd411fedbe47037"},{"version":"92ec74a116d454015264ec3a1238416c800b352c423b0773deee6f5ed6ddce8b","signature":"4536241a2b6b50c093c96b45b6249d9bf51ad460b8a5f5b67b7e012e0d03c05d"},"5b583a7f8b2fc3c7f29e8aebc73c99cbe8df5f6e59a0c2427f90e89f8bdfb13d",{"version":"a2eb6498e964b2418d0d178da23c426dbd28eaa8392aab8d7efaacddea9a5b9f","signature":"e0ab3680a5936bf24b5081dd0ae5b003908dfc8e4084ce97933a95ac96040122"},{"version":"590cdcbea0c9d5bfa62a9b76fc1ad7a2fba8b14a43c08ea8cc3bde29911cbe62","signature":"3ad4ae570b7091713c30b128d700820867f667b6f760f68250d614b2af2856ce"},"c060799d029416006ea24e10c86c8f1cdf2bb32b6fa2478cb4a941798bbb15f7",{"version":"616a980aca196d3779b5b483ddba67c2ce2f7ae0a3c344dd13b400de2e601d7e","signature":"7ad89fdfda1b7df8a652192903101ed2d98e276564d402da7651d72f84f7060e"},{"version":"a3e4d5b357b8059bbe77f1ba4e1b5ac418c887391ac17f752c06fb45750de0e4","signature":"5cf2e7e1c2638fb166e627277ba741ed1fb72ab8112675c1b5ecacea1bdadae9"},{"version":"b8d3773a936faaccb054f7ab53a3794816556df48fc7ff443749e32eba63b94a","signature":"80f5c86c4ec93e5e7eb0110dc1ba5d85115455d39eed616aaa44ae4d58cbc613"},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"19efad8495a7a6b064483fccd1d2b427403dd84e67819f86d1c6ee3d7abf749c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eef826bc4a19de22155487984e345a34c9cd511dd1170edc7a447cb8231dd4a","affectsGlobalScope":true,"impliedFormat":99},{"version":"6bfdbd285cb25907f31ba9db446e61eb2aa55cf46220ebe5fceaa18b2a0b26e8","affectsGlobalScope":true},"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","9e5985581e4bf2d7d7ebebc5464404da39c4eb552ee07bcbceb669c108352a99","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",{"version":"0b026968c66026a85c16aab5581ca03688da142dbc2bc5afcbaec968a8d27350","signature":"1c57ca6c8cb5921c3246069f61d6479fcb659bb2de7406c009aad991aa1c3b9b"},{"version":"c536f8088dfa642e704423872fd8017c3049f6ee2046465ea879494cae0650f1","signature":"2e207df875878acbb125a490076d902de5bc8606b510abb58175ed37c79f9746"},"3ffc7b76098f0c4688a57b9a2948efff274e65571b871f3f6dcc43786a0eddee",{"version":"55aec2fd68e2abfa451013955bd6cac5618f5bc814cb0131d9c3d0e9283dfafa","signature":"8f42eae87de9268f64d1e20cb3359677a1ca99dc0205c993ca00747667674489"},"e3f335a00c1b2165f20e125cb29536b75b4fcc95c1760f2803b95feb2ded15bd",{"version":"73a0ee6395819b063df4b148211985f2e1442945c1a057204cf4cf6281760dc3","affectsGlobalScope":true,"impliedFormat":1},{"version":"d05d8c67116dceafc62e691c47ac89f8f10cf7313cd1b2fb4fe801c2bf1bb1a7","impliedFormat":1},{"version":"3c5bb5207df7095882400323d692957e90ec17323ccff5fd5f29a1ecf3b165d0","impliedFormat":1},{"version":"7959556e748fee75a1ed10822bed4e92edae99ed75e428b650376bacb20ce257","signature":"5a81de12417101e8964d1370944563204a470760be39079613c910b549a0b53a"},"4ca18ab8cd76fb5fb9f7013772f05cdf7b17cdd2f65c39a20399c01c04952a9e",{"version":"a0acca63c9e39580f32a10945df231815f0fe554c074da96ba6564010ffbd2d8","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"3b724a66c071d616203133f8d099a0cb881b0b43fd42e8621e611243c5f30cd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a38efe83ff77c34e0f418a806a01ca3910c02ee7d64212a59d59bca6c2c38fa1","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"3fe4022ba1e738034e38ad9afacbf0f1f16b458ed516326f5bf9e4a31e9be1dc","impliedFormat":1},{"version":"a957197054b074bcdf5555d26286e8461680c7c878040d0f4e2d5509a7524944","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"e9b97d69510658d2f4199b7d384326b7c4053b9e6645f5c19e1c2a54ede427fc","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"f478f6f5902dc144c0d6d7bdc919c5177cac4d17a8ca8653c2daf6d7dc94317f","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"b200675fd112ffef97c166d0341fb33f6e29e9f27660adde7868e95c5bc98beb","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a0a1dda070290b92da5a50113b73ecc4dd6bcbffad66e3c86503d483eafbadcf","impliedFormat":1},{"version":"59dcad36c4549175a25998f6a8b33c1df8e18df9c12ebad1dfb25af13fd4b1ce","impliedFormat":1},{"version":"9ba5b6a30cb7961b68ad4fb18dca148db151c2c23b8d0a260fc18b83399d19d3","impliedFormat":1},{"version":"3f3edb8e44e3b9df3b7ca3219ab539710b6a7f4fe16bd884d441af207e03cd57","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"d71535813e39c23baa113bc4a29a0e187b87d1105ccc8c5a6ebaca38d9a9bff2","impliedFormat":1},{"version":"8cf7e92bdb2862c2d28ba4535c43dc599cfbc0025db5ed9973d9b708dcbe3d98","affectsGlobalScope":true,"impliedFormat":1},{"version":"278e70975bd456bba5874eaee17692355432e8d379b809a97f6af0eee2b702d8","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"636302a00dfd1f9fe6e8e91e4e9350c6518dcc8d51a474e4fc3a9ba07135100b","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"e1ce1d622f1e561f6cdf246372ead3bbc07ce0342024d0e9c7caf3136f712698","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"27e4532aaaa1665d0dd19023321e4dc12a35a741d6b8e1ca3517fcc2544e0efe","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"8c2ad42d5d1a2e8e6112625767f8794d9537f1247907378543106f7ba6c7df90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"98ffdf93dfdd206516971d28e3e473f417a5cfd41172e46b4ce45008f640588e","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"12e8ce658dd17662d82fb0509d2057afc5e6ee30369a2e9e0957eff725b1f11d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74736930d108365d7bbe740c7154706ccfb1b2a3855a897963ab3e5c07ecbf19","impliedFormat":1},{"version":"858f999b3e4a45a4e74766d43030941466460bf8768361d254234d5870480a53","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"63b05afa6121657f25e99e1519596b0826cda026f09372c9100dfe21417f4bd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"3797dd6f4ea3dc15f356f8cdd3128bfa18122213b38a80d6c1f05d8e13cbdad8","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1}],"root":[[157,176],[234,240],[254,273],[306,326],[333,343],347,348],"options":{"allowImportingTsExtensions":true,"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"esModuleInterop":true,"jsx":4,"module":99,"noFallthroughCasesInSwitch":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"strict":true,"target":7,"useDefineForClassFields":true},"referencedMap":[[270,1],[271,2],[272,3],[273,4],[238,1],[240,5],[239,6],[334,7],[335,7],[261,8],[259,9],[260,10],[308,11],[306,12],[307,13],[326,14],[322,15],[323,16],[319,7],[320,17],[317,18],[316,7],[312,19],[315,15],[324,20],[321,21],[314,15],[318,22],[313,15],[325,23],[236,24],[237,25],[235,7],[269,26],[268,9],[336,7],[254,27],[255,28],[256,29],[257,30],[262,7],[263,31],[264,15],[265,32],[311,33],[310,34],[267,35],[266,15],[337,36],[309,37],[338,7],[258,38],[341,39],[340,40],[339,29],[333,41],[342,7],[343,42],[305,43],[304,44],[62,45],[142,46],[140,47],[141,48],[125,49],[93,50],[104,51],[105,52],[106,53],[126,54],[107,55],[128,56],[108,57],[102,58],[111,59],[110,59],[109,60],[123,50],[103,61],[112,62],[117,63],[113,50],[124,7],[127,7],[114,49],[92,64],[94,49],[101,65],[115,66],[121,45],[95,67],[97,67],[120,67],[96,67],[116,49],[98,67],[100,49],[118,49],[119,67],[99,67],[122,59],[78,68],[61,69],[91,70],[65,71],[64,72],[75,45],[68,73],[67,74],[79,75],[80,45],[74,76],[69,45],[71,77],[66,78],[77,79],[70,80],[89,81],[81,82],[73,83],[76,45],[82,84],[90,85],[88,86],[87,86],[86,87],[85,86],[83,86],[84,86],[63,74],[72,88],[57,89],[58,90],[60,91],[59,92],[56,93],[55,45],[394,94],[395,94],[396,95],[355,96],[397,97],[398,98],[399,99],[350,45],[353,100],[351,45],[352,45],[400,101],[401,102],[402,103],[403,104],[404,105],[405,106],[406,106],[408,107],[407,108],[409,109],[410,110],[411,111],[393,112],[354,45],[412,113],[413,114],[414,115],[447,116],[415,117],[416,118],[417,119],[418,120],[419,121],[420,122],[421,123],[422,124],[423,125],[424,126],[425,126],[426,127],[427,45],[428,45],[429,128],[431,129],[430,130],[432,131],[433,132],[434,133],[435,134],[436,135],[437,136],[438,137],[439,138],[440,139],[441,140],[442,141],[443,142],[444,143],[445,144],[446,145],[52,59],[349,59],[48,45],[50,146],[51,59],[253,45],[49,45],[346,147],[345,148],[344,45],[241,59],[252,149],[246,45],[247,59],[245,45],[250,45],[248,150],[249,45],[242,45],[251,45],[244,151],[243,45],[204,152],[231,153],[205,153],[206,153],[193,152],[207,45],[181,152],[208,153],[209,45],[210,152],[211,153],[212,153],[213,45],[214,45],[224,153],[215,153],[192,152],[216,153],[217,45],[218,45],[219,45],[220,153],[194,153],[182,153],[196,153],[197,153],[198,154],[199,153],[200,153],[203,155],[178,45],[232,156],[233,153],[186,153],[190,153],[188,153],[180,45],[221,153],[187,153],[191,45],[222,153],[227,153],[223,153],[184,45],[189,153],[202,153],[225,153],[201,153],[226,153],[195,153],[185,152],[228,153],[183,45],[229,45],[177,45],[230,45],[179,45],[274,45],[289,157],[290,157],[303,158],[291,159],[292,159],[293,160],[287,161],[285,162],[276,45],[280,163],[284,164],[282,165],[288,166],[277,167],[278,168],[279,169],[281,170],[283,171],[286,172],[294,159],[295,159],[296,159],[297,157],[298,159],[299,159],[275,159],[300,45],[302,173],[301,159],[139,174],[130,175],[138,176],[137,176],[129,45],[136,177],[131,178],[132,179],[134,176],[135,180],[133,178],[53,45],[54,45],[46,45],[47,45],[8,45],[9,45],[11,45],[10,45],[2,45],[12,45],[13,45],[14,45],[15,45],[16,45],[17,45],[18,45],[19,45],[3,45],[20,45],[21,45],[4,45],[22,45],[26,45],[23,45],[24,45],[25,45],[27,45],[28,45],[29,45],[5,45],[30,45],[31,45],[32,45],[33,45],[6,45],[37,45],[34,45],[35,45],[36,45],[38,45],[7,45],[39,45],[44,45],[45,45],[40,45],[41,45],[42,45],[43,45],[1,45],[371,181],[381,182],[370,181],[391,183],[362,184],[361,185],[390,186],[384,187],[389,188],[364,189],[378,190],[363,191],[387,192],[359,193],[358,186],[388,194],[360,195],[365,196],[366,45],[369,196],[356,45],[392,197],[382,198],[373,199],[374,200],[376,201],[372,202],[375,203],[385,186],[367,204],[368,205],[377,206],[357,207],[380,198],[379,196],[383,45],[386,208],[332,209],[328,210],[327,45],[329,211],[330,45],[331,212],[156,213],[147,214],[154,215],[149,45],[150,45],[148,216],[151,213],[143,45],[144,45],[155,217],[146,218],[152,45],[153,219],[145,220],[163,221],[164,45],[166,222],[165,45],[157,45],[158,223],[347,224],[348,225],[234,45],[161,226],[172,227],[173,226],[176,228],[174,45],[175,229],[160,230],[162,226],[167,231],[170,232],[159,233],[171,234],[168,235],[169,230]],"semanticDiagnosticsPerFile":[[238,[{"start":59,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305},{"start":167,"length":10,"code":2339,"category":1,"messageText":"Property 'currencies' does not exist on type '{ accounts: string; auth: string; entries: string; transactions: string; user: string; }'."}]],[239,[{"start":9,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305}]],[306,[{"start":124,"length":11,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'AccountType'.","category":1,"code":2305},{"start":137,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305},{"start":1728,"length":8,"code":2322,"category":1,"messageText":{"messageText":"Type 'Resolver<{ type: AccountTypeValue; name: string; commodityId: string; initialBalance: string; description?: string | undefined; }, any, { type: AccountTypeValue; name: string; description: string; commodityId: string & BRAND<...>; initialBalance: string & BRAND<...>; }>' is not assignable to type 'Resolver'.","category":1,"code":2322,"next":[{"messageText":"Types of parameters 'options' and 'options' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'ResolverOptions' is not assignable to type 'ResolverOptions<{ type: AccountTypeValue; name: string; commodityId: string; initialBalance: string; description?: string | undefined; }>'.","category":1,"code":2322,"next":[{"messageText":"Types of property 'names' are incompatible.","category":1,"code":2326,"next":[{"messageText":"Type 'string[] | undefined' is not assignable to type '(\"description\" | requiredKeys<{ commodityId: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>)[] | undefined'.","category":1,"code":2322,"next":[{"messageText":"Type 'string[]' is not assignable to type '(\"description\" | requiredKeys<{ commodityId: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>)[]'.","category":1,"code":2322,"next":[{"messageText":"Type 'string' is not assignable to type '\"description\" | requiredKeys<{ commodityId: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>'.","category":1,"code":2322}]}],"canonicalHead":{"code":2322,"messageText":"Type 'ResolverOptions' is not assignable to type 'ResolverOptions<{ type: AccountTypeValue; name: string; commodityId: string; initialBalance: string; description?: string | undefined; }>'."}}]}]}]}]}},{"start":1822,"length":12,"code":2339,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'AccountDomain'."},{"start":1922,"length":14,"code":2322,"category":1,"messageText":{"messageText":"Type 'number' is not assignable to type 'string & BRAND<\"AmountString\">'.","category":1,"code":2322,"next":[{"messageText":"Type 'number' is not assignable to type 'string'.","category":1,"code":2322}]}},{"start":2587,"length":16,"code":2345,"category":1,"messageText":{"messageText":"Argument of type '(data: AccountCreateDTO) => void' is not assignable to parameter of type 'SubmitHandler'.","category":1,"code":2345,"next":[{"messageText":"Types of parameters 'data' and 'data' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'TFieldValues' is not assignable to type 'AccountCreateDTO'.","category":1,"code":2322,"next":[{"messageText":"Type 'FieldValues' is missing the following properties from type 'AccountCreateDTO': commodityId, description, initialBalance, name, type","category":1,"code":2739,"canonicalHead":{"code":2322,"messageText":"Type 'FieldValues' is not assignable to type 'AccountCreateDTO'."}}]}]}]}},{"start":4205,"length":15,"code":2345,"category":1,"messageText":"Argument of type '\"currency_code\"' is not assignable to parameter of type '\"type\" | \"name\" | \"description\" | \"isSystem\" | \"commodityId\" | \"initialBalance\"'."},{"start":4499,"length":12,"code":2339,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'FieldErrors'."},{"start":4565,"length":12,"code":2339,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'FieldErrors'."}]]],"affectedFilesPendingEmit":[270,271,272,273,238,240,239,334,335,261,259,260,308,306,307,326,322,323,319,320,317,316,312,315,324,321,314,318,313,325,236,237,235,269,268,336,254,255,256,257,262,263,264,265,311,310,267,266,337,309,338,258,341,340,339,342,343,163,164,166,165,157,158,347,348,234,161,172,173,176,174,175,160,162,167,170,159,171,168,169],"emitSignatures":[157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,234,235,236,237,238,239,240,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,334,335,336,337,338,339,340,341,342,343,347,348],"version":"5.8.3"} \ No newline at end of file diff --git a/packages/shared/src/types/account.ts b/packages/shared/src/types/account.ts index b7ce1c5d..1cc4f117 100644 --- a/packages/shared/src/types/account.ts +++ b/packages/shared/src/types/account.ts @@ -27,13 +27,12 @@ export type AccountCreateDTO = { commodityId: UUID; description: string; initialBalance: AmountString; - isSystem?: boolean; name: string; type: AccountTypeValue; }; export type AccountUpdateDTO = Partial< - Pick + Pick >; export type AccountResponseDTO = AccountDomain; diff --git a/packages/shared/src/types/transaction.ts b/packages/shared/src/types/transaction.ts index a0c26fc9..38a4f6fa 100644 --- a/packages/shared/src/types/transaction.ts +++ b/packages/shared/src/types/transaction.ts @@ -62,6 +62,7 @@ export type UpdateTransactionRequestDTO = { }; export type TransactionResponseDTO = { + commodityId: UUID; createdAt: IsoDatetimeString; description: string; id: UUID; diff --git a/packages/shared/src/validation/accounts.ts b/packages/shared/src/validation/accounts.ts index d8ffa988..6d6c98b6 100644 --- a/packages/shared/src/validation/accounts.ts +++ b/packages/shared/src/validation/accounts.ts @@ -5,7 +5,6 @@ import { ACCOUNT_TYPE_VALUES } from "../constants"; import { notNullText, requiredText, - isoDatetime, uuid, amountString, } from "./baseValidations"; @@ -27,15 +26,3 @@ export const accountUpdateSchema = accountCreateSchema type: true, }) .partial(); - -export const accountResponseSchema = z.object({ - createdAt: isoDatetime, - currentClearedBalanceLocal: z.number(), - description: requiredText, - id: uuid, - initialBalance: z.number(), - name: requiredText, - type: accountType, - updatedAt: isoDatetime, - userId: uuid, -}); diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index cc0aa339..fad6ab9d 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -5,13 +5,7 @@ import { MIN_TRANSACTION_OPERATIONS, } from "../constants/transactions"; -import { - uuid, - requiredText, - isoDate, - isoDatetime, - amountString, -} from "./baseValidations"; +import { uuid, requiredText, isoDate, amountString } from "./baseValidations"; // amount — posting in the account's native currency // value — posting in the transaction's currency (GnuCash convention) @@ -56,21 +50,7 @@ export const transactionUpdateSchema = z.object({ version: z.number().int().nonnegative(), }); -export const transactionResponseSchema = z.object({ - createdAt: isoDatetime, - description: requiredText, - id: uuid, - isTombstone: z.boolean(), - postingDate: isoDate, - transactionDate: isoDate, - updatedAt: isoDatetime, - userId: uuid, - version: z.number().int().nonnegative(), -}); - export type OperationCreateInput = z.infer; export type OperationUpdateInput = z.infer; - export type TransactionCreateInput = z.infer; export type TransactionUpdateInput = z.infer; -export type TransactionResponse = z.infer; From 865671df521832f9de5a0fa9c4db281e207ff046 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 14:13:32 +0300 Subject: [PATCH 13/30] LED-66 feat(database): add commodity schema and update related entities --- ...s_ser_duncan.sql => 0000_careful_dust.sql} | 4 +- apps/backend/drizzle/meta/0000_snapshot.json | 12 +- apps/backend/drizzle/meta/_journal.json | 4 +- .../CommodityRepository.interface.ts | 2 +- .../application/mappers/transaction.mapper.ts | 3 +- .../usecases/transaction/CreateTransaction.ts | 15 +- .../__tests__/createTransaction.test.ts | 68 ++----- apps/backend/src/db/schemas/transactions.ts | 2 +- apps/backend/src/db/test-db.ts | 13 +- apps/backend/src/db/test-utils/prettyPrint.ts | 6 +- .../src/db/test-utils/testEntityBuilder.ts | 7 +- apps/backend/src/di/container.ts | 1 - .../transactions/transaction.entity.test.ts | 192 ++++-------------- .../domain/transactions/transaction.entity.ts | 17 +- apps/backend/src/domain/transactions/types.ts | 2 +- .../db/commodities/commodity.repository.ts | 6 +- 16 files changed, 103 insertions(+), 251 deletions(-) rename apps/backend/drizzle/{0000_serious_ser_duncan.sql => 0000_careful_dust.sql} (95%) diff --git a/apps/backend/drizzle/0000_serious_ser_duncan.sql b/apps/backend/drizzle/0000_careful_dust.sql similarity index 95% rename from apps/backend/drizzle/0000_serious_ser_duncan.sql rename to apps/backend/drizzle/0000_careful_dust.sql index f1713a88..e14c575a 100644 --- a/apps/backend/drizzle/0000_serious_ser_duncan.sql +++ b/apps/backend/drizzle/0000_careful_dust.sql @@ -17,7 +17,7 @@ CREATE TABLE `accounts` ( --> statement-breakpoint CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`);--> statement-breakpoint CREATE TABLE `transactions` ( - `valuation_commodity_id` text NOT NULL, + `commodity_id` text NOT NULL, `created_at` text NOT NULL, `description` text NOT NULL, `id` text PRIMARY KEY NOT NULL, @@ -27,7 +27,7 @@ CREATE TABLE `transactions` ( `updated_at` text NOT NULL, `user_id` text NOT NULL, `version` integer NOT NULL, - FOREIGN KEY (`valuation_commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/meta/0000_snapshot.json index 610b6821..07cebc7c 100644 --- a/apps/backend/drizzle/meta/0000_snapshot.json +++ b/apps/backend/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "e16bd8ef-8add-4e77-bb28-3325a74ab237", + "id": "59edadf8-a69b-4dbb-a24a-828a220d6559", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "accounts": { @@ -137,8 +137,8 @@ "transactions": { "name": "transactions", "columns": { - "valuation_commodity_id": { - "name": "valuation_commodity_id", + "commodity_id": { + "name": "commodity_id", "type": "text", "primaryKey": false, "notNull": true, @@ -219,12 +219,12 @@ } }, "foreignKeys": { - "transactions_valuation_commodity_id_commodities_id_fk": { - "name": "transactions_valuation_commodity_id_commodities_id_fk", + "transactions_commodity_id_commodities_id_fk": { + "name": "transactions_commodity_id_commodities_id_fk", "tableFrom": "transactions", "tableTo": "commodities", "columnsFrom": [ - "valuation_commodity_id" + "commodity_id" ], "columnsTo": [ "id" diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index 988a0484..73cfb78c 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1784787321727, - "tag": "0000_serious_ser_duncan", + "when": 1784803897112, + "tag": "0000_careful_dust", "breakpoints": true } ] diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts index c5dae07a..d63d875a 100644 --- a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -2,5 +2,5 @@ import { UUID } from '@ledgerly/shared/types'; import { CommoditySnapshot } from 'src/domain/commodities'; export type CommodityRepositoryInterface = { - getById(userId: string, commodityId: UUID): Promise; + getById(userId: UUID, commodityId: UUID): Promise; }; diff --git a/apps/backend/src/application/mappers/transaction.mapper.ts b/apps/backend/src/application/mappers/transaction.mapper.ts index c44ec135..48a5ef77 100644 --- a/apps/backend/src/application/mappers/transaction.mapper.ts +++ b/apps/backend/src/application/mappers/transaction.mapper.ts @@ -1,5 +1,6 @@ import { Transaction } from 'src/domain'; import { DateValue } from 'src/domain/domain-core/value-objects/DateValue'; +import { Id } from 'src/domain/domain-core/value-objects/Id'; import { CreateTransactionProps, TransactionBuildContext, @@ -39,7 +40,7 @@ export class TransactionMapper { ); return { - commodityId: dto.commodityId, + commodityId: Id.restore(dto.commodityId), description: dto.description, operations, postingDate: DateValue.restore(dto.postingDate), diff --git a/apps/backend/src/application/usecases/transaction/CreateTransaction.ts b/apps/backend/src/application/usecases/transaction/CreateTransaction.ts index e969aaf0..f8759469 100644 --- a/apps/backend/src/application/usecases/transaction/CreateTransaction.ts +++ b/apps/backend/src/application/usecases/transaction/CreateTransaction.ts @@ -3,13 +3,12 @@ import { TransactionResponseDTO, } from 'src/application/dto'; import type { - CommodityRepositoryInterface, TransactionManagerInterface, TransactionRepositoryInterface, } from 'src/application/interfaces'; import { TransactionMapper } from 'src/application/mappers'; import { TransactionContextLoader } from 'src/application/services/TransactionService'; -import { Commodity, User } from 'src/domain'; +import { User } from 'src/domain'; import { Transaction } from 'src/domain/transactions'; export class CreateTransactionUseCase { @@ -17,23 +16,16 @@ export class CreateTransactionUseCase { protected readonly transactionManager: TransactionManagerInterface, protected readonly transactionRepository: TransactionRepositoryInterface, protected readonly transactionContextLoader: TransactionContextLoader, - protected readonly commodityRepository: CommodityRepositoryInterface, ) {} async execute( user: User, data: CreateTransactionRequestDTO, ): Promise { - const { commodityId, operations } = data; const createdTransaction = await this.transactionManager.run(async () => { - const commoditySnapshot = await this.commodityRepository.getById( - user.getId().valueOf(), - commodityId, - ); - const context = await this.transactionContextLoader.loadContext( user, - operations, + data.operations, ); const createTransactionProps = TransactionMapper.toCreateTransactionProps( @@ -41,11 +33,8 @@ export class CreateTransactionUseCase { context, ); - const commodity = Commodity.restore(commoditySnapshot); - const transaction = Transaction.create( user.getId(), - commodity, createTransactionProps, ); diff --git a/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts b/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts index 557ba0b9..86361224 100644 --- a/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts +++ b/apps/backend/src/application/usecases/transaction/__tests__/createTransaction.test.ts @@ -1,5 +1,4 @@ import type { - CommodityRepositoryInterface, TransactionManagerInterface, TransactionRepositoryInterface, } from 'src/application/interfaces'; @@ -27,15 +26,10 @@ describe('CreateTransactionUseCase', () => { loadContext: vi.fn(), }; - const commodityRepository = { - getById: vi.fn(), - }; - const createTransactionUseCase = new CreateTransactionUseCase( transactionManager as unknown as TransactionManagerInterface, mockTransactionRepository as unknown as TransactionRepositoryInterface, transactionContextLoader as unknown as TransactionContextLoader, - commodityRepository as unknown as CommodityRepositoryInterface, ); const transactionRawData = { @@ -61,19 +55,13 @@ describe('CreateTransactionUseCase', () => { describe('execute', () => { it('should create a new transaction with entries successfully', async () => { - const { - transactionContext, - transactionData: { transactionCommodity }, - transactionDTO, - } = TransactionBuilder.request({ - currencies: ['USD'], - operations: operationsData, - settings: transactionRawData, - user, - }); - - commodityRepository.getById.mockResolvedValue( - transactionCommodity.toSnapshot(), + const { transactionContext, transactionDTO } = TransactionBuilder.request( + { + currencies: ['USD'], + operations: operationsData, + settings: transactionRawData, + user, + }, ); transactionContextLoader.loadContext.mockResolvedValue( @@ -161,19 +149,13 @@ describe('CreateTransactionUseCase', () => { const dbError = new Error('Database error'); mockTransactionRepository.create.mockRejectedValue(dbError); - const { - transactionContext, - transactionData: { transactionCommodity }, - transactionDTO, - } = TransactionBuilder.request({ - currencies: ['USD'], - operations: operationsData, - settings: transactionRawData, - user, - }); - - commodityRepository.getById.mockResolvedValue( - transactionCommodity.toSnapshot(), + const { transactionContext, transactionDTO } = TransactionBuilder.request( + { + currencies: ['USD'], + operations: operationsData, + settings: transactionRawData, + user, + }, ); transactionContextLoader.loadContext.mockResolvedValue( @@ -191,25 +173,19 @@ describe('CreateTransactionUseCase', () => { { accountKey: 'USD', amount: '5000', description: '2' }, ]; - const { - transactionContext, - transactionData: { transactionCommodity }, - transactionDTO, - } = TransactionBuilder.request({ - currencies: ['USD'], - operations: unbalancedOperations, - settings: transactionRawData, - user, - }); + const { transactionContext, transactionDTO } = TransactionBuilder.request( + { + currencies: ['USD'], + operations: unbalancedOperations, + settings: transactionRawData, + user, + }, + ); transactionContextLoader.loadContext.mockResolvedValue( transactionContext, ); - commodityRepository.getById.mockResolvedValue( - transactionCommodity.toSnapshot(), - ); - await expect( createTransactionUseCase.execute(user, transactionDTO), ).rejects.toThrow(UnbalancedTransactionError); diff --git a/apps/backend/src/db/schemas/transactions.ts b/apps/backend/src/db/schemas/transactions.ts index 5f63bfc8..7269b673 100644 --- a/apps/backend/src/db/schemas/transactions.ts +++ b/apps/backend/src/db/schemas/transactions.ts @@ -18,7 +18,7 @@ import { usersTable } from './users'; export const transactionsTable = sqliteTable( 'transactions', { - commodityId: text('valuation_commodity_id') + commodityId: text('commodity_id') .notNull() .references(() => commoditiesTable.id) .$type(), diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index 7f413946..3b6b4c4a 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -393,16 +393,13 @@ export class TestDB { precision?: CommodityPrecisionNumber; }, ) => { + const nextName = this.commodityCounter.getNextName({ delimiter: '' }); + const commodityData = { - code: - params?.code ?? - CommodityCode.create( - `COM${this.commodityCounter.getNextName({ delimiter: '' })}`, - ).valueOf(), - name: - params?.name ?? `Commodity ${this.transactionCounter.getNextName()}`, + code: params?.code ?? CommodityCode.create(`COM${nextName}`).valueOf(), + name: params?.name ?? `Commodity ${nextName}`, precision: params?.precision ?? 2, - symbol: params?.symbol ?? `${this.commodityCounter.getNextName()}`, + symbol: params?.symbol ?? `${nextName}`, userId, }; diff --git a/apps/backend/src/db/test-utils/prettyPrint.ts b/apps/backend/src/db/test-utils/prettyPrint.ts index 43113656..b94b1448 100644 --- a/apps/backend/src/db/test-utils/prettyPrint.ts +++ b/apps/backend/src/db/test-utils/prettyPrint.ts @@ -20,8 +20,8 @@ const formatDebitCredit = ( const getAccountInfo = ( accountId: UUID, - accountMap: Map = new Map(), - commoditiesMapById: Map = new Map(), + accountMap?: Map, + commoditiesMapById?: Map, ): { commodity: string; name: string } => { if (!accountMap) { return { commodity: 'N/A', name: 'Unknown Account' }; @@ -32,7 +32,7 @@ const getAccountInfo = ( return { commodity: 'N/A', name: 'Unknown Account' }; } - const commodity = commoditiesMapById.get(account.toSnapshot().commodityId); + const commodity = commoditiesMapById?.get(account.toSnapshot().commodityId); return { commodity: commodity?.toSnapshot().code ?? 'N/A', diff --git a/apps/backend/src/db/test-utils/testEntityBuilder.ts b/apps/backend/src/db/test-utils/testEntityBuilder.ts index 9d6abd36..7fea652d 100644 --- a/apps/backend/src/db/test-utils/testEntityBuilder.ts +++ b/apps/backend/src/db/test-utils/testEntityBuilder.ts @@ -226,7 +226,7 @@ export class TransactionBuilder { private buildTransactionProps(): CreateTransactionProps { return { - commodityId: this.transactionCommodity.getId().valueOf(), + commodityId: this.transactionCommodity.getId(), description: this.description, operations: this.operationsData.map((operation) => { const amount = Amount.create(operation.amount); @@ -283,13 +283,8 @@ export class TransactionBuilder { private buildTransaction(): TransactionBuilderResult { const requestFixture = this.buildRequest(); - const commodity = this.getCommodityById( - requestFixture.transactionDTO.commodityId, - ); - const transaction = Transaction.create( this.user.getId(), - commodity, this.buildTransactionProps(), ); diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index 6c367158..251be844 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -87,7 +87,6 @@ export const createContainer = (db: DataBase): AppContainer => { transactionManager, transactionRepository, transactionContextLoader, - commodityRepository, ); const getTransactionByIdUseCase = new GetTransactionByIdUseCase( diff --git a/apps/backend/src/domain/transactions/transaction.entity.test.ts b/apps/backend/src/domain/transactions/transaction.entity.test.ts index ed33d286..d56ce0b4 100644 --- a/apps/backend/src/domain/transactions/transaction.entity.test.ts +++ b/apps/backend/src/domain/transactions/transaction.entity.test.ts @@ -31,7 +31,6 @@ import { } from 'vitest'; import { Account } from '../accounts'; -import { Commodity } from '../commodities'; import { Amount, DateValue, Id, Version } from '../domain-core'; import { CreateOperationProps, @@ -92,7 +91,7 @@ const toCreateTransactionProps = ( 'getAccountByKey' | 'operationsData' | 'transactionDTO' | 'transactionData' >, ): CreateTransactionProps => ({ - commodityId: fixture.transactionData.commodityId, + commodityId: Id.restore(fixture.transactionData.commodityId), description: fixture.transactionData.description, operations: toCreateOperationProps( fixture.operationsData, @@ -116,7 +115,6 @@ const toMetadataUpdateData = ( describe('Transaction Domain Entity', () => { let user: User; - let commodity: Commodity; let data: TransactionBuilderResult; @@ -200,7 +198,6 @@ describe('Transaction Domain Entity', () => { user, }); - commodity = data.transactionData.transactionCommodity; transactionData = toCreateTransactionProps(data); }); @@ -216,11 +213,7 @@ describe('Transaction Domain Entity', () => { it('should create a valid transaction with operation', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); expect(transaction).toBeInstanceOf(Transaction); expect(transaction.description).toBe(transactionData.description); @@ -246,11 +239,7 @@ describe('Transaction Domain Entity', () => { }); it('should serialize and deserialize correctly', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const transactionSnapshot = transaction.toSnapshot(); @@ -266,11 +255,7 @@ describe('Transaction Domain Entity', () => { }); it('should restore the persisted version as a value object', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const transactionSnapshot = transaction.toSnapshot(); const restoredTransaction = Transaction.restore({ @@ -285,11 +270,7 @@ describe('Transaction Domain Entity', () => { }); it('should restore tombstone operations without allowing them to be updated', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const transactionSnapshot = transaction.toSnapshot(); const [operationToDelete1, operationToDelete2, ...activeOperations] = transactionSnapshot.operations; @@ -353,11 +334,7 @@ describe('Transaction Domain Entity', () => { }); it('should expose full and active snapshots explicitly', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const transactionSnapshot = transaction.toSnapshot(); const operationToDelete = transactionSnapshot.operations.find( @@ -414,11 +391,7 @@ describe('Transaction Domain Entity', () => { describe('Updating Transaction data', () => { it('Should update only description, increment version and update timestamps', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const description = 'Updated transaction description'; const originalSnapshot = transaction.toSnapshot(); @@ -453,11 +426,7 @@ describe('Transaction Domain Entity', () => { }); it('Should update only postingDate, increment version and update timestamps', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const postingDate = DateValue.restore('2024-02-15').valueOf(); @@ -488,11 +457,7 @@ describe('Transaction Domain Entity', () => { }); it('Should update only transactionDate, increment version and update timestamps', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const transactionDate = DateValue.restore('2024-03-10').valueOf(); @@ -520,11 +485,7 @@ describe('Transaction Domain Entity', () => { }); it('Should update all fields at once, increment version and update timestamps', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); @@ -558,11 +519,7 @@ describe('Transaction Domain Entity', () => { describe('Manage Operations', () => { it('rejects attaching an operation that is already attached', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const operation = transaction.getOperations()[0]; const error = captureThrownError(() => @@ -580,15 +537,10 @@ describe('Transaction Domain Entity', () => { }); it('rejects an operation that belongs to another transaction of the same user', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const otherTransaction = Transaction.create( user.getId(), - commodity, transactionData, ); const foreignOperation = otherTransaction.getOperations()[0]; @@ -642,11 +594,7 @@ describe('Transaction Domain Entity', () => { }; it('should reject the same operation ID in update and delete when represented by different Id instances', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const operationId = transaction.getOperations()[0].getId().valueOf(); const error = captureConflictingOperationIdsError(() => @@ -670,11 +618,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject duplicate update IDs represented by different Id instances', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const operationId = transaction.getOperations()[0].getId().valueOf(); const error = captureConflictingOperationIdsError(() => @@ -700,11 +644,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject duplicate delete IDs represented by different Id instances', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const operationId = transaction.getOperations()[0].getId().valueOf(); const error = captureConflictingOperationIdsError(() => @@ -727,11 +667,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject updating a non-existent operation without changing the transaction', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); const unknownOperationId = Id.create(); @@ -758,11 +694,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject deleting a non-existent operation without changing the transaction', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); const unknownOperationId = Id.create(); @@ -789,11 +721,7 @@ describe('Transaction Domain Entity', () => { }); it('Should add a new operation and increase version', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); @@ -879,11 +807,7 @@ describe('Transaction Domain Entity', () => { }); it('should update existing operations and increase the aggregate version once', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); @@ -972,11 +896,7 @@ describe('Transaction Domain Entity', () => { }); it('Should delete an existing operation, keep it in snapshot and hide it from active operations', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); @@ -1039,11 +959,7 @@ describe('Transaction Domain Entity', () => { describe('Deletion', () => { it('should mark transaction as deleted and all related operations, increase version and update timestamps', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); @@ -1069,11 +985,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject updates after transaction is deleted', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); transaction.markAsDeleted(); const deletedSnapshot = transaction.toSnapshot(); @@ -1101,11 +1013,7 @@ describe('Transaction Domain Entity', () => { }); it('should not allow deleting an already deleted transaction', () => { - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); transaction.markAsDeleted(); const originalSnapshot = transaction.toSnapshot(); @@ -1146,7 +1054,7 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); expect(() => - Transaction.create(user.getId(), commodity, transactionData), + Transaction.create(user.getId(), transactionData), ).not.toThrow(); }); @@ -1171,9 +1079,9 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - expect(() => - Transaction.create(user.getId(), commodity, transactionData), - ).toThrow(UnbalancedTransactionError); + expect(() => Transaction.create(user.getId(), transactionData)).toThrow( + UnbalancedTransactionError, + ); }); it('should validate a multi-currency operation patch by value rather than amount', () => { @@ -1196,11 +1104,7 @@ describe('Transaction Domain Entity', () => { }); const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const operationToUpdate = transaction.getOperations()[1]; const error = captureThrownError(() => @@ -1247,9 +1151,9 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - expect(() => - Transaction.create(user.getId(), commodity, transactionData), - ).toThrow(UnbalancedTransactionError); + expect(() => Transaction.create(user.getId(), transactionData)).toThrow( + UnbalancedTransactionError, + ); }); it('should reject update when resulting operations do not sum to zero', () => { @@ -1287,11 +1191,7 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); const originalVersion = transaction.getVersion(); @@ -1331,7 +1231,7 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); const error = captureThrownError(() => - Transaction.create(user.getId(), commodity, transactionData), + Transaction.create(user.getId(), transactionData), ); expect(error).toBeInstanceOf(InsufficientOperationsError); @@ -1362,13 +1262,13 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - expect(() => - Transaction.create(user.getId(), commodity, transactionData), - ).toThrow(InsufficientOperationsError); + expect(() => Transaction.create(user.getId(), transactionData)).toThrow( + InsufficientOperationsError, + ); }); it('should allow creation with the maximum number of operations', () => { - const transaction = Transaction.create(user.getId(), commodity, { + const transaction = Transaction.create(user.getId(), { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS), }); @@ -1380,7 +1280,7 @@ describe('Transaction Domain Entity', () => { it('should reject creation with more than the maximum number of operations', () => { const error = captureThrownError(() => - Transaction.create(user.getId(), commodity, { + Transaction.create(user.getId(), { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS + 1), }), @@ -1419,11 +1319,7 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const originalSnapshot = transaction.toSnapshot(); const originalVersion = transaction.getVersion(); @@ -1446,7 +1342,7 @@ describe('Transaction Domain Entity', () => { }); it('should reject an update that exceeds the maximum number of operations', () => { - const transaction = Transaction.create(user.getId(), commodity, { + const transaction = Transaction.create(user.getId(), { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS), }); @@ -1467,7 +1363,7 @@ describe('Transaction Domain Entity', () => { }); it('should allow replacing operations without exceeding the maximum', () => { - const transaction = Transaction.create(user.getId(), commodity, { + const transaction = Transaction.create(user.getId(), { ...transactionData, operations: createBalancedOperations(MAX_TRANSACTION_OPERATIONS), }); @@ -1513,11 +1409,7 @@ describe('Transaction Domain Entity', () => { const transactionData = toCreateTransactionProps(data); - const transaction = Transaction.create( - user.getId(), - commodity, - transactionData, - ); + const transaction = Transaction.create(user.getId(), transactionData); const initialVersion = transaction.getVersion(); diff --git a/apps/backend/src/domain/transactions/transaction.entity.ts b/apps/backend/src/domain/transactions/transaction.entity.ts index 9c4e6fa6..00597617 100644 --- a/apps/backend/src/domain/transactions/transaction.entity.ts +++ b/apps/backend/src/domain/transactions/transaction.entity.ts @@ -11,7 +11,6 @@ import { UnbalancedTransactionError, } from 'src/domain/domain.errors'; -import { Commodity } from '../commodities'; import { EntityIdentity, EntityTimestamps, @@ -66,24 +65,20 @@ export class Transaction { private readonly ownership: ParentChildRelation, private postingDate: DateValue, private transactionDate: DateValue, - private readonly commodityId: ParentChildRelation, + private readonly commodityRelation: ParentChildRelation, public description: string, private version: Version, ) {} - static create( - userId: Id, - commodity: Commodity, - dto: CreateTransactionProps, - ): Transaction { + static create(userId: Id, dto: CreateTransactionProps): Transaction { const identity = EntityIdentity.create(); const timestamps = EntityTimestamps.create(); const softDelete = SoftDelete.create(); const ownership = ParentChildRelation.create(userId, identity.getId()); const commodityRelation = ParentChildRelation.create( - commodity.getId(), + dto.commodityId, identity.getId(), ); @@ -245,13 +240,17 @@ export class Transaction { return this.ownership.getParentId(); } + getCommodityId(): Id { + return this.commodityRelation.getParentId(); + } + canBeUpdated(): boolean { return !this.isDeleted(); } private buildSnapshot(operations: Operation[]): TransactionSnapshot { return { - commodityId: this.commodityId.getParentId().valueOf(), + commodityId: this.commodityRelation.getParentId().valueOf(), createdAt: this.getCreatedAt().valueOf(), description: this.description, id: this.getId().valueOf(), diff --git a/apps/backend/src/domain/transactions/types.ts b/apps/backend/src/domain/transactions/types.ts index 1eca6601..73fee402 100644 --- a/apps/backend/src/domain/transactions/types.ts +++ b/apps/backend/src/domain/transactions/types.ts @@ -24,7 +24,7 @@ export type CreateTransactionProps = { postingDate: DateValue; transactionDate: DateValue; operations: OperationProps[]; - commodityId: UUID; + commodityId: Id; }; export type OperationsPatch = { diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index 2b195e53..f6f2ee71 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -18,7 +18,11 @@ export class CommodityRepository .select() .from(commoditiesTable) .where( - and(eq(commoditiesTable.userId, userId), eq(commoditiesTable.id, id)), + and( + eq(commoditiesTable.userId, userId), + eq(commoditiesTable.id, id), + eq(commoditiesTable.isTombstone, false), + ), ) .get(); From 9c4eb65cc0c7a5a3f4a4ae4c38667e4d30075a0b Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 16:44:27 +0300 Subject: [PATCH 14/30] LED-66 feat(transactions): add update transaction use case and related tests --- .../application/usecases/transaction/index.ts | 1 + .../src/db/test-utils/prettyPrint.test.ts | 2 +- apps/backend/src/db/test-utils/prettyPrint.ts | 2 +- apps/backend/src/di/container.ts | 27 ++++++---- .../transaction.repository.test.ts | 50 ++++++++++++++++++- .../transaction.integration.test.ts | 9 ++-- docs/DOMAIN.md | 2 +- .../shared/src/validation/transactions.ts | 4 +- 8 files changed, 77 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/application/usecases/transaction/index.ts b/apps/backend/src/application/usecases/transaction/index.ts index cf55c680..ab5e53aa 100644 --- a/apps/backend/src/application/usecases/transaction/index.ts +++ b/apps/backend/src/application/usecases/transaction/index.ts @@ -2,3 +2,4 @@ export { CreateTransactionUseCase } from './CreateTransaction'; export { DeleteTransactionUseCase } from './DeleteTransaction'; export { GetTransactionByIdUseCase } from './GetTransactionById'; export { GetAllTransactionsUseCase } from './GetAllTransactions'; +export { UpdateTransactionUseCase } from './UpdateTransaction'; diff --git a/apps/backend/src/db/test-utils/prettyPrint.test.ts b/apps/backend/src/db/test-utils/prettyPrint.test.ts index 7c1e069e..14826775 100644 --- a/apps/backend/src/db/test-utils/prettyPrint.test.ts +++ b/apps/backend/src/db/test-utils/prettyPrint.test.ts @@ -80,7 +80,7 @@ describe('prettyPrint', () => { expect(result).toBeUndefined(); expect(consoleInfo).toHaveBeenCalledOnce(); expect(consoleInfo).toHaveBeenCalledWith( - expect.stringContaining('Currency'), + expect.stringContaining('Commodity'), ); expect(consoleInfo).toHaveBeenCalledWith(expect.stringContaining('Debit')); expect(consoleInfo).toHaveBeenCalledWith(expect.stringContaining('Credit')); diff --git a/apps/backend/src/db/test-utils/prettyPrint.ts b/apps/backend/src/db/test-utils/prettyPrint.ts index b94b1448..b80dbab8 100644 --- a/apps/backend/src/db/test-utils/prettyPrint.ts +++ b/apps/backend/src/db/test-utils/prettyPrint.ts @@ -93,7 +93,7 @@ function formatTransactionPTA( `${transaction.getTransactionDate().valueOf()} ${transaction.description}`, ); lines.push( - ` ${'Account'.padEnd(20)} ${'Description'.padEnd(50)} ${'Currency'.padEnd(8)} ${'Debit'.padStart(amountColumnWidth)} ${'Credit'.padStart(amountColumnWidth)}`, + ` ${'Account'.padEnd(20)} ${'Description'.padEnd(50)} ${'Commodity'.padEnd(8)} ${'Debit'.padStart(amountColumnWidth)} ${'Credit'.padStart(amountColumnWidth)}`, ); transaction.getOperations().forEach((operation) => { lines.push(formatOperationPTA(operation, accountMap, commoditiesMapById)); diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index 251be844..e9e5feee 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -1,16 +1,21 @@ import { LoginUserUseCase, RegisterUserUseCase } from 'src/application'; import { TransactionContextLoader } from 'src/application/services/TransactionService'; import { ensureEntityExistsAndOwned } from 'src/application/shared/ensureEntityExistsAndOwned'; -import { CreateAccountUseCase } from 'src/application/usecases/accounts/createAccount'; -import { DeleteAccountUseCase } from 'src/application/usecases/accounts/deleteAccount'; -import { GetAccountByIdUseCase } from 'src/application/usecases/accounts/getAccountById'; -import { GetAllAccountsUseCase } from 'src/application/usecases/accounts/getAllAccounts'; -import { UpdateAccountUseCase } from 'src/application/usecases/accounts/updateAccount'; -import { CreateTransactionUseCase } from 'src/application/usecases/transaction/CreateTransaction'; -import { DeleteTransactionUseCase } from 'src/application/usecases/transaction/DeleteTransaction'; -import { GetAllTransactionsUseCase } from 'src/application/usecases/transaction/GetAllTransactions'; -import { GetTransactionByIdUseCase } from 'src/application/usecases/transaction/GetTransactionById'; -import { UpdateTransactionUseCase } from 'src/application/usecases/transaction/UpdateTransaction'; +import { + CreateAccountUseCase, + DeleteAccountUseCase, + GetAccountByIdUseCase, + GetAllAccountsUseCase, + UpdateAccountUseCase, +} from 'src/application/usecases/accounts/'; +import {} from 'src/application/usecases/accounts/deleteAccount'; +import { + CreateTransactionUseCase, + DeleteTransactionUseCase, + GetAllTransactionsUseCase, + GetTransactionByIdUseCase, + UpdateTransactionUseCase, +} from 'src/application/usecases/transaction/'; import { DataBase } from 'src/db'; import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; import { @@ -21,7 +26,7 @@ import { AccountRepository, UserRepository, } from 'src/infrastructure/db'; -import { CommodityRepository } from 'src/infrastructure/db/commodities/commodity.repository'; +import { CommodityRepository } from 'src/infrastructure/db'; import { AccountController, AuthController, diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts index 9815178c..e28ef813 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts @@ -1,5 +1,6 @@ import { UUID } from '@ledgerly/shared/types'; -import { OperationDbRow, UserDbRow } from 'src/db/schema'; +import { eq } from 'drizzle-orm'; +import { commoditiesTable, OperationDbRow, UserDbRow } from 'src/db/schema'; import { TestDB } from 'src/db/test-db'; import { compareEntities, @@ -279,6 +280,53 @@ describe('TransactionRepository', () => { ).rejects.toThrow(RepositoryNotFoundError); }); + it('should not create a transaction when commodity does not exist', async () => { + const transaction = TransactionBuilder.persistence({ + currencies: ['USD'], + operations: [ + { + accountKey: 'USD', + amount: '-200', + description: 'Credit operation', + }, + { + accountKey: 'USD', + amount: '200', + description: 'Debit operation', + }, + ], + settings: { description }, + user: UserPersistenceMapper.toDomain(user), + }).transaction; + + await expect( + transactionRepository.create(user.id, transaction), + ).rejects.toThrow(RepositoryNotFoundError); + }); + + it('should not create a transaction when commodity belongs to another user', async () => { + const otherUser = await testDB.createUser(); + const transaction = data.transaction; + + await expect( + transactionRepository.create(otherUser.id, transaction), + ).rejects.toThrow(RepositoryNotFoundError); + }); + + it('should not create a transaction when commodity is tombstoned', async () => { + const transaction = data.transaction; + const commodityId = transaction.toSnapshot().commodityId; + + await testDB.db + .update(commoditiesTable) + .set({ isTombstone: true }) + .where(eq(commoditiesTable.id, commodityId)); + + await expect( + transactionRepository.create(user.id, transaction), + ).rejects.toThrow(RepositoryNotFoundError); + }); + it('should create a new transaction', async () => { const transaction = data.transaction; diff --git a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts index 38393a14..fe0e0166 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts @@ -374,10 +374,11 @@ describe('Transactions Integration Tests', () => { email: 'otheruser@example.com', password: 'password123', }); + const otherUserCommodity = await testDB.createCommodity(otherUser.id); const otherUserAccount = await testDB.createAccount( otherUser.id, - commodityId, + otherUserCommodity.id, { name: 'Other User Account', }, @@ -1798,10 +1799,11 @@ describe('Transactions Integration Tests', () => { const createdOperations = transaction.operations; const otherUser = await testDB.createUser(); + const otherUserCommodity = await testDB.createCommodity(otherUser.id); const otherUserAccount = await testDB.createAccount( otherUser.id, - commodityId, + otherUserCommodity.id, { name: 'Other User Account', }, @@ -1884,10 +1886,11 @@ describe('Transactions Integration Tests', () => { const createdOperations = transaction.operations; const otherUser = await testDB.createUser(); + const otherUserCommodity = await testDB.createCommodity(otherUser.id); const otherUserAccount = await testDB.createAccount( otherUser.id, - commodityId, + otherUserCommodity.id, { name: 'Other User Account', }, diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index 0900e24f..e2cb16b5 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -341,7 +341,7 @@ Balance: `sum(value) = 0` ✓ — currently this phase is not implemented. ``` Transaction - id: UUID -- valuationCommodityId: UUID (FK) -- denominates operation value fields +- сommodityId: UUID (FK) -- denominates operation value fields - description: string - transactionDate: date (ISO string) - postingDate: date (ISO string) diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index fad6ab9d..9ede40d4 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -7,8 +7,8 @@ import { import { uuid, requiredText, isoDate, amountString } from "./baseValidations"; -// amount — posting in the account's native currency -// value — posting in the transaction's currency (GnuCash convention) +// amount — posting in the account Commodity +// value — posting in the transaction valuation Commodity (GnuCash convention) // For same-currency transactions amount === value. // Transaction balance is validated by summing value across all operations (must equal 0). export const operationCreateSchema = z.object({ From fead7704193a96c3daeb941306de30d435e92fdb Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 18:01:02 +0300 Subject: [PATCH 15/30] LED-67 feat(commodities): implement CRUD operations for commodity repository --- .../CommodityRepository.interface.ts | 26 ++ .../src/application/interfaces/index.ts | 6 +- .../commodities/commodity.repository.test.ts | 299 +++++++++++++++++- .../db/commodities/commodity.repository.ts | 130 +++++++- 4 files changed, 458 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts index d63d875a..3b931385 100644 --- a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -1,6 +1,32 @@ import { UUID } from '@ledgerly/shared/types'; import { CommoditySnapshot } from 'src/domain/commodities'; +export type CommodityRepositoryUpdateInput = Pick< + CommoditySnapshot, + 'updatedAt' +> & + Partial>; + +export type CommodityRepositorySoftDeleteInput = Pick< + CommoditySnapshot, + 'updatedAt' +>; + export type CommodityRepositoryInterface = { getById(userId: UUID, commodityId: UUID): Promise; + getAll(userId: UUID): Promise; + create( + userId: UUID, + commodity: CommoditySnapshot, + ): Promise; + update( + userId: UUID, + commodityId: UUID, + commodity: CommodityRepositoryUpdateInput, + ): Promise; + delete( + userId: UUID, + commodityId: UUID, + data: CommodityRepositorySoftDeleteInput, + ): Promise; }; diff --git a/apps/backend/src/application/interfaces/index.ts b/apps/backend/src/application/interfaces/index.ts index 62dd85a8..78dca4c3 100644 --- a/apps/backend/src/application/interfaces/index.ts +++ b/apps/backend/src/application/interfaces/index.ts @@ -17,4 +17,8 @@ export type { TransactionContext, } from './TransactionManager.interface'; export type { OperationRepositoryInterface } from './OperationRepository.interface'; -export type { CommodityRepositoryInterface } from './CommodityRepository.interface'; +export type { + CommodityRepositoryInterface, + CommodityRepositoryUpdateInput, + CommodityRepositorySoftDeleteInput, +} from './CommodityRepository.interface'; diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 9ece0f8b..0acc65c1 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -1,8 +1,14 @@ import { apiErrorCodes } from '@ledgerly/shared/types'; +import { type CommodityRepositoryUpdateInput } from 'src/application'; import type { CommodityDbRow } from 'src/db/schemas/commodities'; import { UserDbRow } from 'src/db/schemas/users'; +import { CommoditySnapshot } from 'src/domain/commodities/types'; +import { CommodityCode, Name, Timestamp } from 'src/domain/domain-core/'; import { Id } from 'src/domain/domain-core/value-objects/Id'; -import { RepositoryNotFoundError } from 'src/infrastructure/errors'; +import { + RecordAlreadyExistsError, + RepositoryNotFoundError, +} from 'src/infrastructure/errors'; import { describe, vi, beforeEach, it, expect } from 'vitest'; import { TestDB } from '../../../db/test-db'; @@ -76,4 +82,295 @@ describe('CommodityRepository', () => { ).rejects.toThrowError(RepositoryNotFoundError); }); }); + + describe('getAll', () => { + it('should return all commodities for the user', async () => { + const userCommodities = await Promise.all([ + testDB.createCommodity(user.id), + testDB.createCommodity(user.id), + ]); + + const commodities = await commodityRepository.getAll(user.id); + + expect(commodities).toEqual(expect.arrayContaining(userCommodities)); + + expect(commodities).toHaveLength(userCommodities.length); + }); + + it('should return an empty array when the user has no commodities', async () => { + const anotherUser = await testDB.createUser(); + + const commodities = await commodityRepository.getAll(anotherUser.id); + + expect(commodities).toEqual([]); + }); + + it('should not return tombstoned commodities', async () => { + const commodity1 = await testDB.createCommodity(user.id); + const commodity2 = await testDB.createCommodity(user.id); + + await commodityRepository.delete(user.id, commodity1.id, { + updatedAt: Timestamp.create().valueOf(), + }); + + const commodities = await commodityRepository.getAll(user.id); + + expect(commodities).toEqual(expect.arrayContaining([commodity2])); + expect(commodities).not.toEqual(expect.arrayContaining([commodity1])); + }); + + it('should not return commodities belonging to other users', async () => { + const commodity1 = await testDB.createCommodity(user.id); + const anotherUser = await testDB.createUser(); + const commodity2 = await testDB.createCommodity(anotherUser.id); + + const commodities = await commodityRepository.getAll(user.id); + + expect(commodities).toEqual(expect.arrayContaining([commodity1])); + expect(commodities).not.toEqual(expect.arrayContaining([commodity2])); + }); + }); + + describe('create', () => { + it('should create a new commodity for the user', async () => { + const newCommodityData: CommoditySnapshot = { + code: CommodityCode.create('NEW').valueOf(), + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: Name.create('New Commodity').valueOf(), + precision: 2, + symbol: 'N', + updatedAt: Timestamp.create().valueOf(), + userId: user.id, + }; + + const createdCommodity = await commodityRepository.create( + user.id, + newCommodityData, + ); + + expect(createdCommodity).toMatchObject(newCommodityData); + + const retrievedCommodity = await commodityRepository.getById( + user.id, + createdCommodity.id, + ); + + expect(retrievedCommodity).toEqual(createdCommodity); + }); + + it('should throw an error when trying to create a commodity with an existing CommodityCode', async () => { + const createdCommodity = await testDB.createCommodity(user.id); + + const newCommodityData: CommoditySnapshot = { + code: createdCommodity.code, + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: Name.create('Duplicate Commodity').valueOf(), + precision: 2, + symbol: 'D', + updatedAt: Timestamp.create().valueOf(), + userId: user.id, + }; + + await expect( + commodityRepository.create(user.id, newCommodityData), + ).rejects.toThrowError( + new RecordAlreadyExistsError({ + context: { + field: 'code', + tableName: 'commodities', + value: newCommodityData.code, + }, + }), + ); + }); + + it('should allow creating commodities with the same CommodityCode for different users', async () => { + const createdCommodity = await testDB.createCommodity(user.id); + + const anotherUser = await testDB.createUser(); + + const newCommodityData: CommoditySnapshot = { + code: createdCommodity.code, + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: Name.create('Same Code Commodity').valueOf(), + precision: 2, + symbol: 'S', + updatedAt: Timestamp.create().valueOf(), + userId: anotherUser.id, + }; + + const createdCommodityForAnotherUser = await commodityRepository.create( + anotherUser.id, + newCommodityData, + ); + + expect(createdCommodityForAnotherUser).toMatchObject(newCommodityData); + + const retrievedCommodity = await commodityRepository.getById( + anotherUser.id, + createdCommodityForAnotherUser.id, + ); + + expect(retrievedCommodity).toEqual(createdCommodityForAnotherUser); + }); + }); + + describe('update', () => { + let commodityDbRow: CommodityDbRow; + + beforeEach(async () => { + commodityDbRow = await testDB.createCommodity(user.id); + }); + + it('should update the commodity when it exists', async () => { + const updatedData: Partial = { + code: CommodityCode.create('UPD').valueOf(), + name: Name.create('Updated Commodity').valueOf(), + updatedAt: Timestamp.create().valueOf(), + }; + + const updatedCommodity = await commodityRepository.update( + user.id, + commodityDbRow.id, + { ...commodityDbRow, ...updatedData }, + ); + + expect(updatedCommodity.name).toEqual(updatedData.name); + expect(updatedCommodity.code).toEqual(updatedData.code); + expect(updatedCommodity.updatedAt).toEqual(updatedData.updatedAt); + + const retrievedCommodity = await commodityRepository.getById( + user.id, + commodityDbRow.id, + ); + + expect(retrievedCommodity).toEqual(updatedCommodity); + }); + + it('should throw an error when the commodity does not exist', async () => { + const nonExistentId = Id.create().valueOf(); + + const updatedData: CommodityRepositoryUpdateInput = { + code: CommodityCode.create('UPD').valueOf(), + name: Name.create('Updated Commodity').valueOf(), + updatedAt: Timestamp.create().valueOf(), + }; + + await expect( + commodityRepository.update(user.id, nonExistentId, updatedData), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('should throw an error when trying to update a commodity with an existing CommodityCode', async () => { + const existingCommodity = await testDB.createCommodity(user.id); + + const updatedData: CommodityRepositoryUpdateInput = { + code: existingCommodity.code, + name: Name.create('Updated Commodity').valueOf(), + updatedAt: Timestamp.create().valueOf(), + }; + + await expect( + commodityRepository.update(user.id, commodityDbRow.id, updatedData), + ).rejects.toThrowError( + new RecordAlreadyExistsError({ + context: { + field: 'code', + tableName: 'commodities', + value: updatedData.code, + }, + }), + ); + }); + + it('should throw an error when the commodity belongs to a different user', async () => { + const anotherUser = await testDB.createUser(); + + const updatedData: CommodityRepositoryUpdateInput = { + code: CommodityCode.create('UPD').valueOf(), + name: Name.create('Updated Commodity').valueOf(), + updatedAt: Timestamp.create().valueOf(), + }; + + await expect( + commodityRepository.update( + anotherUser.id, + commodityDbRow.id, + updatedData, + ), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('should throw an error when trying to update tombstoned commodity', async () => { + await commodityRepository.delete(user.id, commodityDbRow.id, { + updatedAt: Timestamp.create().valueOf(), + }); + + const updatedData: CommodityRepositoryUpdateInput = { + code: CommodityCode.create('UPD').valueOf(), + name: Name.create('Updated Commodity').valueOf(), + updatedAt: Timestamp.create().valueOf(), + }; + + await expect( + commodityRepository.update(user.id, commodityDbRow.id, updatedData), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + }); + + describe('delete', () => { + let commodityDbRow: CommodityDbRow; + + beforeEach(async () => { + commodityDbRow = await testDB.createCommodity(user.id); + }); + + it('should delete the commodity when it exists', async () => { + await commodityRepository.delete(user.id, commodityDbRow.id, { + updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), + }); + + await expect( + commodityRepository.getById(user.id, commodityDbRow.id), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('should throw an error when the commodity does not exist', async () => { + const nonExistentId = Id.create().valueOf(); + + await expect( + commodityRepository.delete(user.id, nonExistentId, { + updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), + }), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('should throw an error when the commodity belongs to a different user', async () => { + const anotherUser = await testDB.createUser(); + + await expect( + commodityRepository.delete(anotherUser.id, commodityDbRow.id, { + updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), + }), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('should throw an error when trying to delete a commodity that is already tombstoned', async () => { + await commodityRepository.delete(user.id, commodityDbRow.id, { + updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), + }); + + await expect( + commodityRepository.delete(user.id, commodityDbRow.id, { + updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), + }), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + }); }); diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index f6f2ee71..e8dffb5e 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -1,6 +1,10 @@ import { UUID } from '@ledgerly/shared/types'; import { and, eq } from 'drizzle-orm'; -import { type CommodityRepositoryInterface } from 'src/application'; +import { + type CommodityRepositoryInterface, + type CommodityRepositoryUpdateInput, + type CommodityRepositorySoftDeleteInput, +} from 'src/application'; import { commoditiesTable } from 'src/db/schemas/commodities'; import { CommoditySnapshot } from 'src/domain/commodities/types'; @@ -35,4 +39,128 @@ export class CommodityRepository return CommodityPersistenceMapper.toSnapshot(existingCommodity); }, 'Failed to fetch commodity'); } + + getAll(userId: UUID): Promise { + return this.executeDatabaseOperation(async () => { + const commodities = await this.db + .select() + .from(commoditiesTable) + .where( + and( + eq(commoditiesTable.userId, userId), + eq(commoditiesTable.isTombstone, false), + ), + ) + .all(); + + return commodities.map((commodity) => + CommodityPersistenceMapper.toSnapshot(commodity), + ); + }, 'Failed to fetch commodities'); + } + + create( + userId: UUID, + commodity: CommoditySnapshot, + ): Promise { + return this.executeDatabaseOperation( + async () => { + const commodityRow = CommodityPersistenceMapper.toDBRowFromSnapshot({ + ...commodity, + userId, + }); + + const createdCommodity = await this.db + .insert(commoditiesTable) + .values(commodityRow) + .returning() + .get(); + + return CommodityPersistenceMapper.toSnapshot(createdCommodity); + }, + 'Failed to create commodity', + { + unique: { + field: 'code', + tableName: 'commodities', + value: commodity.code ?? 'No code provided', + }, + }, + ); + } + + update( + userId: UUID, + commodityId: UUID, + commodity: CommodityRepositoryUpdateInput, + ): Promise { + return this.executeDatabaseOperation( + async () => { + const safeData = this.getSafeUpdate(commodity, [ + 'code', + 'symbol', + 'name', + 'updatedAt', + ]); + + const updatedCommodity = await this.db + .update(commoditiesTable) + .set(safeData) + .where( + and( + eq(commoditiesTable.userId, userId), + eq(commoditiesTable.id, commodityId), + eq(commoditiesTable.isTombstone, false), + ), + ) + .returning() + .get(); + + const existingCommodity = this.ensureEntityExists( + updatedCommodity, + `Commodity with ID ${commodityId} not found`, + this.entityNotFoundContext('commodity', commodityId), + ); + + return CommodityPersistenceMapper.toSnapshot(existingCommodity); + }, + 'Failed to update commodity', + { + unique: { + field: 'code', + tableName: 'commodities', + value: commodity.code ?? 'No code provided', + }, + }, + ); + } + + delete( + userId: UUID, + commodityId: UUID, + data: CommodityRepositorySoftDeleteInput, + ): Promise { + return this.executeDatabaseOperation(async () => { + const deletedCommodity = await this.db + .update(commoditiesTable) + .set({ isTombstone: true, updatedAt: data.updatedAt }) + .where( + and( + eq(commoditiesTable.userId, userId), + eq(commoditiesTable.id, commodityId), + eq(commoditiesTable.isTombstone, false), + ), + ) + .returning() + .get(); + + const existingCommodity = this.ensureEntityExists( + deletedCommodity, + `Commodity with ID ${commodityId} not found after deletion`, + this.entityNotFoundContext('commodity', commodityId), + ); + + return CommodityPersistenceMapper.toSnapshot(existingCommodity); + }, 'Failed to delete commodity'); + } } From 7086de2f407cdc8ad9e537628fbb6d54986c98ba Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 19:23:17 +0300 Subject: [PATCH 16/30] LED-67 test(commodities): add tests for create and update commodity methods --- .../commodities/commodity.repository.test.ts | 57 +++++++++++++++++++ .../db/commodities/commodity.repository.ts | 4 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 0acc65c1..8a49c02f 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -219,6 +219,40 @@ describe('CommodityRepository', () => { expect(retrievedCommodity).toEqual(createdCommodityForAnotherUser); }); + + it('should associate a created commodity with the userId argument', async () => { + const anotherUser = await testDB.createUser(); + + const newCommodityData: CommoditySnapshot = { + code: CommodityCode.create('OWN').valueOf(), + createdAt: Timestamp.create().valueOf(), + id: Id.create().valueOf(), + isTombstone: false, + name: Name.create('Owned Commodity').valueOf(), + precision: 2, + symbol: 'O', + updatedAt: Timestamp.create().valueOf(), + userId: anotherUser.id, + }; + + const createdCommodity = await commodityRepository.create( + user.id, + newCommodityData, + ); + + expect(createdCommodity.userId).toBe(user.id); + + await expect( + commodityRepository.getById(anotherUser.id, createdCommodity.id), + ).rejects.toThrowError(RepositoryNotFoundError); + + await expect( + commodityRepository.getById(user.id, createdCommodity.id), + ).resolves.toMatchObject({ + id: newCommodityData.id, + userId: user.id, + }); + }); }); describe('update', () => { @@ -267,6 +301,29 @@ describe('CommodityRepository', () => { ).rejects.toThrowError(RepositoryNotFoundError); }); + it('should partially update only the provided fields', async () => { + const updatedData: CommodityRepositoryUpdateInput = { + name: Name.create('Renamed Commodity').valueOf(), + updatedAt: Timestamp.create().valueOf(), + }; + + const updatedCommodity = await commodityRepository.update( + user.id, + commodityDbRow.id, + updatedData, + ); + + expect(updatedCommodity).toMatchObject({ + code: commodityDbRow.code, + id: commodityDbRow.id, + name: updatedData.name, + precision: commodityDbRow.precision, + symbol: commodityDbRow.symbol, + updatedAt: updatedData.updatedAt, + userId: commodityDbRow.userId, + }); + }); + it('should throw an error when trying to update a commodity with an existing CommodityCode', async () => { const existingCommodity = await testDB.createCommodity(user.id); diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index e8dffb5e..eb476d8f 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -83,7 +83,7 @@ export class CommodityRepository unique: { field: 'code', tableName: 'commodities', - value: commodity.code ?? 'No code provided', + value: commodity.code, }, }, ); @@ -129,7 +129,7 @@ export class CommodityRepository unique: { field: 'code', tableName: 'commodities', - value: commodity.code ?? 'No code provided', + value: commodity.code, }, }, ); From 5e08428d52d1b8f442954140d8be6d40e046de2c Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 19:50:47 +0300 Subject: [PATCH 17/30] LED-69 feat(accounts): implement repository-enforced commodity reference validation --- .../usecases/accounts/createAccount.ts | 13 ++- apps/backend/src/db/createTestUser.ts | 15 ++-- .../src/db/test-utils/testEntityBuilder.ts | 15 ++-- .../src/domain/accounts/account.entity.ts | 27 +++--- .../src/domain/accounts/account.test.ts | 80 ++++++++--------- apps/backend/src/domain/accounts/types.ts | 12 +++ docs/DATABASE_SCHEMA.md | 5 +- docs/DOMAIN.md | 5 ++ ...ity-registry-before-currency-validation.md | 3 + .../adr/0016-backend-request-flow.md | 3 + ...enforced-commodity-reference-validation.md | 85 +++++++++++++++++++ docs/architecture/adr/README.md | 1 + 12 files changed, 181 insertions(+), 83 deletions(-) create mode 100644 docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md diff --git a/apps/backend/src/application/usecases/accounts/createAccount.ts b/apps/backend/src/application/usecases/accounts/createAccount.ts index 066dc2af..e1af0c40 100644 --- a/apps/backend/src/application/usecases/accounts/createAccount.ts +++ b/apps/backend/src/application/usecases/accounts/createAccount.ts @@ -26,14 +26,13 @@ export class CreateAccountUseCase { const commodity = Commodity.restore(commoditySnapshot); - const account = Account.create( - user, - commodity, - Name.create(name), + const account = Account.create(user, { + commodityId: commodity.getId(), description, - Amount.create(initialBalance), - AccountType.create(type), - ); + initialBalance: Amount.create(initialBalance), + name: Name.create(name), + type: AccountType.create(type), + }); const accountSnapshot = account.toSnapshot(); diff --git a/apps/backend/src/db/createTestUser.ts b/apps/backend/src/db/createTestUser.ts index 4399d6c2..ac35d353 100644 --- a/apps/backend/src/db/createTestUser.ts +++ b/apps/backend/src/db/createTestUser.ts @@ -63,12 +63,11 @@ export const createAccount = ( ) => { const commodity = params.commodity ?? createCommodity(user); - return Account.create( - user, - commodity, - Name.create(params.name ?? 'Test Account'), - params.description ?? 'Account for testing', - Amount.create('0'), - AccountType.create('asset'), - ); + return Account.create(user, { + commodityId: commodity.getId(), + description: params.description ?? 'Account for testing', + initialBalance: Amount.create('0'), + name: Name.create(params.name ?? 'Test Account'), + type: AccountType.create('asset'), + }); }; diff --git a/apps/backend/src/db/test-utils/testEntityBuilder.ts b/apps/backend/src/db/test-utils/testEntityBuilder.ts index 7fea652d..815fa42b 100644 --- a/apps/backend/src/db/test-utils/testEntityBuilder.ts +++ b/apps/backend/src/db/test-utils/testEntityBuilder.ts @@ -160,14 +160,13 @@ export class TransactionBuilder { code, ); - const account = Account.create( - this.user, - commodity, - Name.create(`Account ${commodityCode}`), - `Account ${commodityCode}`, - Amount.create('0'), - AccountType.create('asset'), - ); + const account = Account.create(this.user, { + commodityId: commodity.getId(), + description: `Account ${commodityCode}`, + initialBalance: Amount.create('0'), + name: Name.create(`Account ${commodityCode}`), + type: AccountType.create('asset'), + }); this.accounts.set(commodityCode, account); this.accountsMap.set(account.getId().valueOf(), account); diff --git a/apps/backend/src/domain/accounts/account.entity.ts b/apps/backend/src/domain/accounts/account.entity.ts index 6669a829..cce73e92 100644 --- a/apps/backend/src/domain/accounts/account.entity.ts +++ b/apps/backend/src/domain/accounts/account.entity.ts @@ -13,7 +13,11 @@ import { DeletedEntityOperationError } from '../domain.errors'; import { User } from '../users/user.entity'; import { AccountType } from './account-type.enum'; -import { AccountSnapshot, AccountUpdateProps } from './types'; +import { + AccountSnapshot, + AccountUpdateProps, + CreateAccountProps, +} from './types'; export class Account { static readonly entityType = 'account'; @@ -32,14 +36,7 @@ export class Account { public isSystem: boolean, ) {} - static create( - user: User, - commodity: Commodity, - name: Name, - description: string, - initialBalance: Amount, - type: AccountType, - ): Account { + static create(user: User, dto: CreateAccountProps): Account { const identity = EntityIdentity.create(); const timestamps = EntityTimestamps.create(); const softDelete = SoftDelete.create(); @@ -50,11 +47,11 @@ export class Account { ); const commodityRelation = ParentChildRelation.create( - commodity.getId(), + dto.commodityId, identity.getId(), ); - const isSystem = type.isSystemType(); + const isSystem = dto.type.isSystemType(); return new Account( identity, @@ -62,11 +59,11 @@ export class Account { softDelete, ownership, commodityRelation, - name, - description, - initialBalance, + dto.name, + dto.description, + dto.initialBalance, Amount.create('0'), - type, + dto.type, isSystem, ); } diff --git a/apps/backend/src/domain/accounts/account.test.ts b/apps/backend/src/domain/accounts/account.test.ts index bd4aeb4f..29e1c808 100644 --- a/apps/backend/src/domain/accounts/account.test.ts +++ b/apps/backend/src/domain/accounts/account.test.ts @@ -42,14 +42,13 @@ describe('Account Domain Entity', () => { describe('create method', () => { it('should create account with valid data', () => { - const account = Account.create( - user, - commodity, + const account = Account.create(user, { + commodityId: commodity.getId(), + description: 'account-description', + initialBalance: Amount.create('0'), name, - 'account-description', - Amount.create('0'), - accountType, - ); + type: accountType, + }); expect(account).toBeInstanceOf(Account); expect(account.getId()).toBeDefined(); @@ -108,14 +107,13 @@ describe('Account Domain Entity', () => { describe('account management', () => { it('should update account with valid value', () => { - const account = Account.create( - user, - commodity, - Name.create('initial-name'), - 'description', - Amount.create('0'), - accountType, - ); + const account = Account.create(user, { + commodityId: commodity.getId(), + description: 'description', + initialBalance: Amount.create('0'), + name, + type: accountType, + }); account.update({ name: 'updated-name' }); @@ -125,14 +123,13 @@ describe('Account Domain Entity', () => { describe('softDelete method', () => { it('should mark account as tombstone', () => { - const account = Account.create( - user, - commodity, + const account = Account.create(user, { + commodityId: commodity.getId(), + description: 'account-description', + initialBalance: Amount.create('0'), name, - 'account-description', - Amount.create('0'), - accountType, - ); + type: accountType, + }); expect(account.isDeleted()).toBe(false); @@ -142,14 +139,13 @@ describe('Account Domain Entity', () => { }); it('should not allow updates after soft deletion', () => { - const account = Account.create( - user, - commodity, + const account = Account.create(user, { + commodityId: commodity.getId(), + description: 'account-description', + initialBalance: Amount.create('0'), name, - 'account-description', - Amount.create('0'), - accountType, - ); + type: accountType, + }); account.markAsDeleted(); @@ -159,14 +155,13 @@ describe('Account Domain Entity', () => { }); it('should not allow deleting an already deleted account', () => { - const account = Account.create( - user, - commodity, + const account = Account.create(user, { + commodityId: commodity.getId(), + description: 'account-description', + initialBalance: Amount.create('0'), name, - 'account-description', - Amount.create('0'), - accountType, - ); + type: accountType, + }); account.markAsDeleted(); @@ -195,14 +190,13 @@ describe('Account Domain Entity', () => { vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); - const account = Account.create( - user, - commodity, + const account = Account.create(user, { + commodityId: commodity.getId(), + description: 'account-description', + initialBalance: Amount.create('0'), name, - 'account-description', - Amount.create('0'), - accountType, - ); + type: accountType, + }); expect(account.isDeleted()).toBe(false); diff --git a/apps/backend/src/domain/accounts/types.ts b/apps/backend/src/domain/accounts/types.ts index 3a4ca80b..89112e0b 100644 --- a/apps/backend/src/domain/accounts/types.ts +++ b/apps/backend/src/domain/accounts/types.ts @@ -5,6 +5,10 @@ import { UUID, } from '@ledgerly/shared/types'; +import { Amount, Id, Name } from '../domain-core'; + +import { AccountType } from './account-type.enum'; + export type AccountSnapshot = { createdAt: IsoDatetimeString; currentClearedBalanceLocal: AmountString; @@ -25,3 +29,11 @@ export type AccountUpdateProps = Partial<{ name: string; type: AccountTypeValue; }>; + +export type CreateAccountProps = { + commodityId: Id; + description: string; + initialBalance: Amount; + name: Name; + type: AccountType; +}; diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 287b1be7..27070ba2 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -336,8 +336,9 @@ USERS (root entity) ### Data Integrity -1. **User ownership**: accounts and transactions may reference only Commodities - owned by the same user. +1. **User ownership**: accounts and transactions may reference only active + Commodities owned by the same user. Write repositories enforce this check + immediately before persisting Commodity-backed references. 2. **Required relations**: each operation must have a transaction and account. 3. **Soft deletes**: entities use `isTombstone` where the deletion must be retained in storage. diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index e2cb16b5..16c825bd 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -206,6 +206,11 @@ repository interfaces and transaction boundaries. Mappers own conversion between domain snapshots, read models, response DTOs and persistence shapes. Repositories own persistence access. +Commodity-backed account and transaction writes are a documented boundary +exception: repositories enforce Commodity existence, ownership and tombstone +state immediately before persistence. See +[ADR 0019](./architecture/adr/0019-repository-enforced-commodity-reference-validation.md). + New endpoint operations should be implemented as application use cases. `apps/backend/src/application/services/*` is reserved for helper orchestration used by use cases. Root-level `apps/backend/src/services/*` is legacy and must diff --git a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md index 9d979723..2acf1aa1 100644 --- a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md +++ b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md @@ -101,6 +101,8 @@ Those can be introduced by later ADRs if needed. - Account creation must require a Commodity; account Commodity changes are not allowed after creation. - Transaction creation must require a valuation Commodity. +- The validation boundary for Commodity-backed account and transaction writes is + defined separately in ADR 0019. - Existing API and DTO surfaces may need compatibility fields while data and clients migrate from currency strings to Commodity ids. - Amount validation should use Commodity precision rather than assuming all @@ -119,6 +121,7 @@ Those can be introduced by later ADRs if needed. - [LED-69: Validate Commodity existence, ownership, and archive state](https://gorushkin.atlassian.net/browse/LED-69) - [LED-105: Remove `/currencies` endpoint stub](https://gorushkin.atlassian.net/browse/LED-105) - [LED-70: Replace LED-47 validation with Commodity validation](https://gorushkin.atlassian.net/browse/LED-70) +- [ADR 0019: Repository-Enforced Commodity Reference Validation](./0019-repository-enforced-commodity-reference-validation.md) - [Multicurrency Design](../../MULTICURRENCY_DESIGN.md) - [Domain Model](../../DOMAIN.md) - [Database Schema](../../DATABASE_SCHEMA.md) diff --git a/docs/architecture/adr/0016-backend-request-flow.md b/docs/architecture/adr/0016-backend-request-flow.md index df21ef92..933a262c 100644 --- a/docs/architecture/adr/0016-backend-request-flow.md +++ b/docs/architecture/adr/0016-backend-request-flow.md @@ -72,6 +72,9 @@ Responsibilities: - Owns persistence access and DB-specific query/update behavior. - Implements application repository interfaces where a boundary exists. - Does not create public API response contracts directly. +- May enforce persistence-reference policies that must be checked immediately + before writes, such as Commodity existence, ownership and tombstone state for + Commodity-backed account and transaction writes. See ADR 0019. 6. Application services diff --git a/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md b/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md new file mode 100644 index 00000000..d59719f9 --- /dev/null +++ b/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md @@ -0,0 +1,85 @@ +# ADR 0019: Repository-Enforced Commodity Reference Validation + +- Status: Accepted +- Date: 2026-07-23 +- Jira: https://gorushkin.atlassian.net/browse/LED-69 +- PR: TBD + +## Context + +ADR 0006 defines Commodity identity and requires accounts and transactions to +reference user-owned Commodities by stable id rather than currency code strings. + +There are two possible places to validate those references: + +- application use cases can load the referenced Commodity before creating the + aggregate; +- write repositories can validate the referenced Commodity immediately before + inserting or updating the row that stores the foreign key. + +The account and transaction write paths need the same boundary. Keeping account +validation in the use case while transaction validation lives in the repository +creates an avoidable asymmetry. + +## Decision + +Commodity reference validation for account and transaction writes is enforced by +write repositories. + +Before persisting a new account or transaction, the repository must verify that +the referenced Commodity: + +- exists; +- belongs to the same user as the write; +- is not tombstoned. + +Use cases remain responsible for request orchestration, aggregate construction, +transaction boundaries and calling repository interfaces. They do not need to +load the Commodity solely to prove that the persistence reference is valid. + +Repository checks are part of the repository contract, not a substitute for +request shape validation. HTTP/request schemas still validate that the supplied +Commodity id has the expected UUID shape. + +## Alternatives Considered + +1. Application-level validation before aggregate creation + +- Pros: makes the policy visible in use cases and avoids entering repository + write paths for invalid references. +- Cons: duplicates repository-side data checks, introduces a time-of-check to + time-of-use gap and makes account and transaction flows carry Commodity data + that is not otherwise needed for aggregate behavior. + +2. Rely only on database foreign keys + +- Pros: smallest implementation. +- Cons: foreign keys do not express same-user ownership or active/tombstone + policy and produce less intentional error contracts. + +3. Repository-enforced validation + +- Pros: validates closest to the write, keeps account and transaction write + paths symmetric and gives repositories one clear contract for Commodity-backed + persistence references. +- Cons: invalid references reach the repository call, so tests must assert + stable repository errors rather than expecting use cases to short-circuit. + +## Consequences + +- `CreateAccountUseCase` and `CreateTransactionUseCase` should not duplicate + Commodity existence/ownership/tombstone checks solely for reference integrity. +- Account and transaction repositories must keep explicit pre-write checks for + Commodity ownership and tombstone state. +- Tests for missing, foreign and tombstoned Commodity references belong at the + repository/write-boundary level, with higher-level tests added only where they + verify public error mapping or user-visible behavior. +- LED-69 should be read as Commodity-backed write reference validation, not as a + requirement that failed validation must avoid all repository calls. + +## Related + +- [ADR 0006: Commodity Registry Before Currency Validation](./0006-commodity-registry-before-currency-validation.md) +- [ADR 0016: Backend Request Flow](./0016-backend-request-flow.md) +- [LED-64: Commodity Registry Epic](https://gorushkin.atlassian.net/browse/LED-64) +- [LED-69: Validate Commodity existence, ownership, and archive state](https://gorushkin.atlassian.net/browse/LED-69) diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 113e87b8..00dc99b2 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -49,3 +49,4 @@ - [ADR 0016: Backend request flow](./0016-backend-request-flow.md) - [ADR 0017: Backend HTTP adapter boundary](./0017-backend-http-adapter-boundary.md) - [ADR 0018: Backend HTTP error boundary](./0018-backend-http-error-boundary.md) +- [ADR 0019: Repository-enforced Commodity reference validation](./0019-repository-enforced-commodity-reference-validation.md) From 086575118d1c71541f9e76e83704b74f08f4feef Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 20:12:47 +0300 Subject: [PATCH 18/30] LED-69 feat(accounts): implement account creation with commodity reference validation --- .../src/application/mappers/account.mapper.ts | 23 ++++++++- .../accounts/__tests__/createAccount.test.ts | 47 +------------------ .../usecases/accounts/createAccount.ts | 28 ++--------- apps/backend/src/di/container.ts | 5 +- .../src/domain/accounts/account.entity.ts | 14 +++--- docs/architecture/adr/README.md | 2 +- 6 files changed, 36 insertions(+), 83 deletions(-) diff --git a/apps/backend/src/application/mappers/account.mapper.ts b/apps/backend/src/application/mappers/account.mapper.ts index eae91e68..f2fb5e65 100644 --- a/apps/backend/src/application/mappers/account.mapper.ts +++ b/apps/backend/src/application/mappers/account.mapper.ts @@ -1,5 +1,15 @@ -import { AccountResponseDTO, AccountUpdateDTO } from '@ledgerly/shared/types'; -import { AccountSnapshot, AccountUpdateProps } from 'src/domain/accounts'; +import { + AccountCreateDTO, + AccountResponseDTO, + AccountUpdateDTO, +} from '@ledgerly/shared/types'; +import { + AccountSnapshot, + AccountType, + AccountUpdateProps, +} from 'src/domain/accounts'; +import { CreateAccountProps } from 'src/domain/accounts/types'; +import { Amount, Id, Name } from 'src/domain/domain-core'; export class AccountMapper { static toResponseDTOFromSnapshot( @@ -20,6 +30,15 @@ export class AccountMapper { userId: snapshot.userId, }; } + static toCreateAccountProps(dto: AccountCreateDTO): CreateAccountProps { + return { + commodityId: Id.restore(dto.commodityId), + description: dto.description, + initialBalance: Amount.create(dto.initialBalance), + name: Name.create(dto.name), + type: AccountType.create(dto.type), + }; + } static toUpdateProps(dto: AccountUpdateDTO): AccountUpdateProps { return { diff --git a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts index 70e6cf20..4b43eecf 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts @@ -1,8 +1,4 @@ -import { - AccountRepositoryInterface, - CommodityRepositoryInterface, - EntityNotFoundError, -} from 'src/application'; +import { AccountRepositoryInterface } from 'src/application'; import { createUser } from 'src/db/createTestUser'; import { Commodity } from 'src/domain'; import { Amount, CommodityCode, Name } from 'src/domain/domain-core'; @@ -15,9 +11,6 @@ describe('CreateAccountUseCase', async () => { let createAccountUseCase: CreateAccountUseCase; - const commodityRepository = { - getById: vi.fn(), - }; const accountRepository = { create: vi.fn(), }; @@ -39,7 +32,6 @@ describe('CreateAccountUseCase', async () => { beforeEach(() => { createAccountUseCase = new CreateAccountUseCase( accountRepository as unknown as AccountRepositoryInterface, - commodityRepository as unknown as CommodityRepositoryInterface, ); }); @@ -48,7 +40,6 @@ describe('CreateAccountUseCase', async () => { // Arrange const mockedCommoditySnapshot = commodity.toSnapshot(); - commodityRepository.getById.mockResolvedValue(mockedCommoditySnapshot); // Act const result = await createAccountUseCase.execute(user, { @@ -59,11 +50,6 @@ describe('CreateAccountUseCase', async () => { type, }); - expect(commodityRepository.getById).toHaveBeenCalledWith( - user.getId().valueOf(), - mockedCommoditySnapshot.id, - ); - expect(accountRepository.create).toHaveBeenCalledWith({ commodityId: result.commodityId, createdAt: result.createdAt, @@ -88,36 +74,5 @@ describe('CreateAccountUseCase', async () => { userId: user.getId().valueOf(), }); }); - - it('should throw an error if the commodity does not belong to the user', async () => { - // Arrange - const anotherUser = await createUser(); - const anotherCommodity = Commodity.create( - anotherUser, - Name.create('Another Commodity'), - CommodityCode.create('ANOTHER'), - 2, - null, - ); - const mockedAnotherCommoditySnapshot = anotherCommodity.toSnapshot(); - - commodityRepository.getById.mockRejectedValue( - new EntityNotFoundError({ - entityId: mockedAnotherCommoditySnapshot.id, - entityType: 'commodity', - }), - ); - - // Act & Assert - await expect( - createAccountUseCase.execute(user, { - commodityId: mockedAnotherCommoditySnapshot.id, - description, - initialBalance, - name, - type, - }), - ).rejects.toThrowError(EntityNotFoundError); - }); }); }); diff --git a/apps/backend/src/application/usecases/accounts/createAccount.ts b/apps/backend/src/application/usecases/accounts/createAccount.ts index e1af0c40..838a963c 100644 --- a/apps/backend/src/application/usecases/accounts/createAccount.ts +++ b/apps/backend/src/application/usecases/accounts/createAccount.ts @@ -1,39 +1,21 @@ import { AccountCreateDTO, AccountResponseDTO } from '@ledgerly/shared/types'; -import type { - AccountRepositoryInterface, - CommodityRepositoryInterface, -} from 'src/application/interfaces'; +import type { AccountRepositoryInterface } from 'src/application/interfaces'; import { AccountMapper } from 'src/application/mappers'; -import { AccountType, Account, Commodity } from 'src/domain/'; -import { Amount, Name } from 'src/domain/domain-core'; +import { Account } from 'src/domain/'; import { User } from 'src/domain/users/user.entity'; - export class CreateAccountUseCase { constructor( protected readonly accountRepository: AccountRepositoryInterface, - protected readonly commodityRepository: CommodityRepositoryInterface, ) {} async execute( user: User, data: AccountCreateDTO, ): Promise { - const { commodityId, description, initialBalance, name, type } = data; - - const commoditySnapshot = await this.commodityRepository.getById( - user.getId().valueOf(), - commodityId, + const account = Account.create( + user, + AccountMapper.toCreateAccountProps(data), ); - const commodity = Commodity.restore(commoditySnapshot); - - const account = Account.create(user, { - commodityId: commodity.getId(), - description, - initialBalance: Amount.create(initialBalance), - name: Name.create(name), - type: AccountType.create(type), - }); - const accountSnapshot = account.toSnapshot(); await this.accountRepository.create(accountSnapshot); diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index e9e5feee..92f63f32 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -75,10 +75,7 @@ export const createContainer = (db: DataBase): AppContainer => { }; // Create Account Use Cases - const createAccountUseCase = new CreateAccountUseCase( - accountRepository, - commodityRepository, - ); + const createAccountUseCase = new CreateAccountUseCase(accountRepository); const getAllAccountsUseCase = new GetAllAccountsUseCase(accountRepository); const getAccountByIdUseCase = new GetAccountByIdUseCase(accountRepository); const updateAccountUseCase = new UpdateAccountUseCase(accountRepository); diff --git a/apps/backend/src/domain/accounts/account.entity.ts b/apps/backend/src/domain/accounts/account.entity.ts index cce73e92..070e853e 100644 --- a/apps/backend/src/domain/accounts/account.entity.ts +++ b/apps/backend/src/domain/accounts/account.entity.ts @@ -36,7 +36,7 @@ export class Account { public isSystem: boolean, ) {} - static create(user: User, dto: CreateAccountProps): Account { + static create(user: User, props: CreateAccountProps): Account { const identity = EntityIdentity.create(); const timestamps = EntityTimestamps.create(); const softDelete = SoftDelete.create(); @@ -47,11 +47,11 @@ export class Account { ); const commodityRelation = ParentChildRelation.create( - dto.commodityId, + props.commodityId, identity.getId(), ); - const isSystem = dto.type.isSystemType(); + const isSystem = props.type.isSystemType(); return new Account( identity, @@ -59,11 +59,11 @@ export class Account { softDelete, ownership, commodityRelation, - dto.name, - dto.description, - dto.initialBalance, + props.name, + props.description, + props.initialBalance, Amount.create('0'), - dto.type, + props.type, isSystem, ); } diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 00dc99b2..0489a2c3 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -49,4 +49,4 @@ - [ADR 0016: Backend request flow](./0016-backend-request-flow.md) - [ADR 0017: Backend HTTP adapter boundary](./0017-backend-http-adapter-boundary.md) - [ADR 0018: Backend HTTP error boundary](./0018-backend-http-error-boundary.md) -- [ADR 0019: Repository-enforced Commodity reference validation](./0019-repository-enforced-commodity-reference-validation.md) +- [ADR 0019: Repository-Enforced Commodity Reference Validation](./0019-repository-enforced-commodity-reference-validation.md) From d611a9945c4c45736e398a44c15ec8e9f380c961 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 20:32:16 +0300 Subject: [PATCH 19/30] LED-70 refactor(accounts): update test descriptions and variable names for clarity --- .../usecases/accounts/__tests__/updateAccount.test.ts | 2 +- apps/backend/src/domain/domain-core/README.md | 4 ++-- .../src/presentation/http/accounts/account.controller.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts index c7e4fb66..f096d1ac 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/updateAccount.test.ts @@ -116,7 +116,7 @@ describe('UpdateAccount', async () => { // - should throw error when user does not exist // - should throw error when account does not exist // - should throw error when account does not belong to user - // - should validate and update different fields (name, description, type, originalCurrency) + // - should validate and update different fields (name, description, type, commodityId) // - should handle Account domain validation errors // - should handle repository errors properly }); diff --git a/apps/backend/src/domain/domain-core/README.md b/apps/backend/src/domain/domain-core/README.md index 5e5a053a..a1fcb971 100644 --- a/apps/backend/src/domain/domain-core/README.md +++ b/apps/backend/src/domain/domain-core/README.md @@ -17,7 +17,7 @@ Value Objects - immutable objects that are described by their attributes, not by - **`Id`** - unique identifier - **`Timestamp`** - timestamp - **`Amount`** - signed integer minor-unit monetary amount without currency -- **`Currency`** - currency +- **`CommodityCode`** - display/search code for a user-owned Commodity - **`DateValue`** - date value - **`Name`** - name/title - **`Email`** - email address with validation @@ -156,7 +156,7 @@ console.log(name.valueOf()); // 'John Doe' // Amount for monetary operations const amount = Amount.create('100.50'); -const currency = Currency.create('USD'); +const commodityCode = CommodityCode.create('USD'); // Value Objects comparison const email1 = Email.create('TEST@example.com'); diff --git a/apps/backend/src/presentation/http/accounts/account.controller.test.ts b/apps/backend/src/presentation/http/accounts/account.controller.test.ts index 420ae229..feaf00c9 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.test.ts @@ -160,8 +160,8 @@ describe('AccountController', () => { it('should handle invalid requestBody gracefully', async () => { const invalidRequestBody = { + commodityId: null, name: 123, - originalCurrency: null, type: 'invalid-type', }; From 8e71d8ebe27e7373ae653f029c88aa747e9668a1 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 20:41:55 +0300 Subject: [PATCH 20/30] LED-68 feat(accounts): add isTombstone handling in account creation and update tests --- apps/backend/src/db/test-db.ts | 4 +++- .../db/accounts/account.repository.test.ts | 23 ++++++++++++++++--- .../http/accounts/account.controller.test.ts | 2 +- .../http/accounts/account.integration.test.ts | 18 +++++++-------- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index 3b6b4c4a..76adc731 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -391,12 +391,14 @@ export class TestDB { symbol?: CommoditySymbolString; name?: string; precision?: CommodityPrecisionNumber; + isTombstone?: boolean; }, ) => { const nextName = this.commodityCounter.getNextName({ delimiter: '' }); const commodityData = { code: params?.code ?? CommodityCode.create(`COM${nextName}`).valueOf(), + isTombstone: params?.isTombstone ?? false, name: params?.name ?? `Commodity ${nextName}`, precision: params?.precision ?? 2, symbol: params?.symbol ?? `${nextName}`, @@ -413,7 +415,7 @@ export class TestDB { userId: commodityData.userId, ...TestDB.createTimestamps, ...TestDB.uuid, - isTombstone: false, + isTombstone: commodityData.isTombstone, }) .returning() .get(); diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts index e520ae5c..89fc2896 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts @@ -187,9 +187,26 @@ describe('AccountRepository', () => { expect(account1.userId).not.toBe(account2.userId); }); - it.todo('should throw an error if the original currency does not exist'); + it('should throw an error if commodity is tombstoned', async () => { + const tombstonedCommodity = await testDB.createCommodity(user.id, { + code: CommodityCode.create('TOMBSTONED').valueOf(), + isTombstone: true, + name: 'Tombstoned Commodity', + precision: 2, + symbol: 'T', + }); - it.todo('should handle UUID collision gracefully'); + const newAccount = getAccountData({ + commodityId: tombstonedCommodity.id, + name: 'New Account with Tombstoned Commodity', + type: 'asset', + userId: user.id, + }); + + await expect(accountRepository.create(newAccount)).rejects.toThrowError( + RepositoryNotFoundError, + ); + }); }); describe('getAll', () => { @@ -406,7 +423,7 @@ describe('AccountRepository', () => { expect(updatedSecondUserAccount?.name).toBe(accountData.name); }); - it.todo('should validate currency when updating'); + it.todo('should preserve commodityId when updating account fields'); it('should only update allowed fields', async () => { const maliciousData = { diff --git a/apps/backend/src/presentation/http/accounts/account.controller.test.ts b/apps/backend/src/presentation/http/accounts/account.controller.test.ts index feaf00c9..01f18bb3 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.test.ts @@ -124,7 +124,7 @@ describe('AccountController', () => { it('should handle invalid requestBody gracefully', async () => { const invalidRequestBody = { - currency: null, + commodityId: null, name: 123, type: 'invalid-type', }; diff --git a/apps/backend/src/presentation/http/accounts/account.integration.test.ts b/apps/backend/src/presentation/http/accounts/account.integration.test.ts index 2069b45b..4d93c82c 100644 --- a/apps/backend/src/presentation/http/accounts/account.integration.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.integration.test.ts @@ -201,7 +201,6 @@ describe('Accounts Integration Tests', () => { const accountToUpdate = accounts[0]; const updatedData = { - currency: 'EUR', name: 'Updated Account Name', }; @@ -250,10 +249,12 @@ describe('Accounts Integration Tests', () => { // - should return 400 when name is empty // - should return 400 when name is missing // - should return 400 when name is not a string - // - should return 400 when currency is empty - // - should return 400 when currency is missing - // - should return 400 when currency is invalid format - // - should return 400 when currency doesn't exist in database + // - should return 400 when commodityId is empty + // - should return 400 when commodityId is missing + // - should return 400 when commodityId is invalid UUID format + // - should return 404 when commodityId doesn't exist in database + // - should return 404 when commodityId belongs to another user + // - should return 404 when commodityId points to an archived Commodity // - should return 400 when type is empty // - should return 400 when type is invalid enum value // - should return 400 when type is missing @@ -269,8 +270,6 @@ describe('Accounts Integration Tests', () => { describe.todo('PUT /api/accounts/:id - Validation', () => { // - should return 400 when name is empty string // - should return 400 when name is not a string - // - should return 400 when currency is empty - // - should return 400 when currency doesn't exist in database // - should return 400 when type is invalid enum value // - should return 400 when description is not a string // - should return 404 when account ID doesn't exist @@ -299,8 +298,9 @@ describe('Accounts Integration Tests', () => { // Foreign Key Constraint Tests describe.todo('Foreign Key Constraints', () => { // - should handle user deletion cascading to accounts - // - should prevent creation with non-existent currency - // - should prevent update with non-existent currency + // - should prevent creation with non-existent commodityId + // - should prevent creation with another user's commodityId + // - should prevent creation with archived commodityId }); // Business Logic Tests From 3f2b6e2ddb11a16ac22a47d91159f97fa61d3226 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Thu, 23 Jul 2026 22:07:08 +0300 Subject: [PATCH 21/30] LED-110 feat(commodities): add createCommodityUseCase --- .../CommodityRepository.interface.ts | 5 +- .../application/mappers/commodity.mapper.ts | 37 +++++++ apps/backend/src/application/mappers/index.ts | 1 + .../accounts/__tests__/createAccount.test.ts | 15 ++- .../__tests__/createCommodity.test.ts | 65 ++++++++++++ .../usecases/commodities/createCommodity.ts | 29 ++++++ .../application/usecases/commodities/index.ts | 1 + .../backend/src/application/usecases/index.ts | 1 + apps/backend/src/db/createTestUser.ts | 9 +- .../src/db/test-utils/testEntityBuilder.ts | 9 +- .../src/domain/accounts/account.test.ts | 13 ++- .../domain/commodities/commodity.entity.ts | 29 +++--- .../src/domain/commodities/commodity.test.ts | 99 +++++++------------ apps/backend/src/domain/commodities/index.ts | 6 +- apps/backend/src/domain/commodities/types.ts | 16 ++- .../commodities/commodity.repository.test.ts | 48 +-------- .../db/commodities/commodity.repository.ts | 11 +-- packages/shared/src/routes/index.ts | 2 +- packages/shared/src/types/account.ts | 32 +++--- packages/shared/src/types/commodity.ts | 27 +++++ packages/shared/src/types/index.ts | 1 + packages/shared/src/types/types.ts | 5 +- 22 files changed, 277 insertions(+), 184 deletions(-) create mode 100644 apps/backend/src/application/mappers/commodity.mapper.ts create mode 100644 apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts create mode 100644 apps/backend/src/application/usecases/commodities/createCommodity.ts create mode 100644 apps/backend/src/application/usecases/commodities/index.ts create mode 100644 packages/shared/src/types/commodity.ts diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts index 3b931385..18078ba4 100644 --- a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -15,10 +15,7 @@ export type CommodityRepositorySoftDeleteInput = Pick< export type CommodityRepositoryInterface = { getById(userId: UUID, commodityId: UUID): Promise; getAll(userId: UUID): Promise; - create( - userId: UUID, - commodity: CommoditySnapshot, - ): Promise; + create(commodity: CommoditySnapshot): Promise; update( userId: UUID, commodityId: UUID, diff --git a/apps/backend/src/application/mappers/commodity.mapper.ts b/apps/backend/src/application/mappers/commodity.mapper.ts new file mode 100644 index 00000000..bf871df0 --- /dev/null +++ b/apps/backend/src/application/mappers/commodity.mapper.ts @@ -0,0 +1,37 @@ +import { + CommodityCreateDTO, + CommodityResponseDTO, +} from '@ledgerly/shared/types'; +import { + CommoditySnapshot, + CreateCommodityProps, +} from 'src/domain/commodities'; + +export class CommodityMapper { + static toResponseDTOFromSnapshot( + snapshot: CommoditySnapshot, + ): CommodityResponseDTO { + return { + code: snapshot.code, + createdAt: snapshot.createdAt, + id: snapshot.id, + isTombstone: snapshot.isTombstone, + name: snapshot.name, + precision: snapshot.precision, + symbol: snapshot.symbol, + updatedAt: snapshot.updatedAt, + userId: snapshot.userId, + }; + } + + static toCreateCommodityProps( + props: CommodityCreateDTO, + ): CreateCommodityProps { + return { + code: props.code, + name: props.name, + precision: props.precision, + symbol: props.symbol, + }; + } +} diff --git a/apps/backend/src/application/mappers/index.ts b/apps/backend/src/application/mappers/index.ts index e0a3c286..f19cddd9 100644 --- a/apps/backend/src/application/mappers/index.ts +++ b/apps/backend/src/application/mappers/index.ts @@ -3,3 +3,4 @@ export { OperationMapper } from './operation.mapper'; export { TransactionReadModelResponseMapper } from './transaction-read-model-response.mapper'; export { TransactionMapper } from './transaction.mapper'; export { UserMapper } from './user.mapper'; +export { CommodityMapper } from './commodity.mapper'; diff --git a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts index 4b43eecf..357391c1 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts @@ -1,7 +1,7 @@ import { AccountRepositoryInterface } from 'src/application'; import { createUser } from 'src/db/createTestUser'; import { Commodity } from 'src/domain'; -import { Amount, CommodityCode, Name } from 'src/domain/domain-core'; +import { Amount, CommodityCode } from 'src/domain/domain-core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CreateAccountUseCase } from '../createAccount'; @@ -20,13 +20,12 @@ describe('CreateAccountUseCase', async () => { const initialBalance = Amount.create('1000').valueOf(); const currentClearedBalanceLocal = Amount.create('0').valueOf(); - const commodity = Commodity.create( - user, - Name.create('Test Commodity'), - CommodityCode.create('TEST'), - 2, - null, - ); + const commodity = Commodity.create(user, { + code: CommodityCode.create('USD').valueOf(), + name: 'Test Commodity', + precision: 2, + symbol: null, + }); const type = 'asset'; beforeEach(() => { diff --git a/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts new file mode 100644 index 00000000..eff2e419 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts @@ -0,0 +1,65 @@ +import { CommodityCreateDTO } from '@ledgerly/shared/types'; +import { CommodityRepositoryInterface } from 'src/application'; +import { createUser } from 'src/db/createTestUser'; +import { User } from 'src/domain'; +import { Name, CommodityCode } from 'src/domain/domain-core/'; +import { beforeEach, describe, expect, it, vi, beforeAll } from 'vitest'; + +import { CreateCommodityUseCase } from '../createCommodity'; + +describe('CreateCommodityUseCase', () => { + let user: User; + let createCommodityUseCase: CreateCommodityUseCase; + + const commodityRepository = { + create: vi.fn(), + }; + + beforeAll(async () => { + user = await createUser(); + }); + + beforeEach(() => { + createCommodityUseCase = new CreateCommodityUseCase( + commodityRepository as unknown as CommodityRepositoryInterface, + ); + }); + + describe('execute', () => { + it('should create a new commodity successfully', async () => { + // Arrange + + const data: CommodityCreateDTO = { + code: CommodityCode.create('TEST').valueOf(), + name: Name.create('Test Commodity').valueOf(), + precision: 2, + symbol: null, + }; + + // Act + const result = await createCommodityUseCase.execute(user, data); + + // Assert + expect(commodityRepository.create).toHaveBeenCalledWith({ + code: result.code, + createdAt: result.createdAt, + id: result.id, + isTombstone: false, + name: result.name, + precision: result.precision, + symbol: result.symbol, + updatedAt: result.updatedAt, + userId: result.userId, + }); + + expect(result).toMatchObject({ + code: data.code, + isTombstone: false, + name: data.name, + precision: data.precision, + symbol: data.symbol, + userId: user.getId().valueOf(), + }); + }); + }); +}); diff --git a/apps/backend/src/application/usecases/commodities/createCommodity.ts b/apps/backend/src/application/usecases/commodities/createCommodity.ts new file mode 100644 index 00000000..42aaa7e5 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/createCommodity.ts @@ -0,0 +1,29 @@ +import { + CommodityCreateDTO, + CommodityResponseDTO, +} from '@ledgerly/shared/types'; +import { CommodityRepositoryInterface } from 'src/application/interfaces'; +import { CommodityMapper } from 'src/application/mappers/commodity.mapper'; +import { Commodity } from 'src/domain/commodities'; +import { User } from 'src/domain/users/user.entity'; + +export class CreateCommodityUseCase { + constructor( + protected readonly commodityRepository: CommodityRepositoryInterface, + ) {} + async execute( + user: User, + data: CommodityCreateDTO, + ): Promise { + const commodity = Commodity.create( + user, + CommodityMapper.toCreateCommodityProps(data), + ); + + const commoditySnapshot = commodity.toSnapshot(); + + await this.commodityRepository.create(commoditySnapshot); + + return CommodityMapper.toResponseDTOFromSnapshot(commoditySnapshot); + } +} diff --git a/apps/backend/src/application/usecases/commodities/index.ts b/apps/backend/src/application/usecases/commodities/index.ts new file mode 100644 index 00000000..064dd0f9 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/index.ts @@ -0,0 +1 @@ +export { CreateCommodityUseCase } from './createCommodity'; diff --git a/apps/backend/src/application/usecases/index.ts b/apps/backend/src/application/usecases/index.ts index ad3786b0..6f6e1a17 100644 --- a/apps/backend/src/application/usecases/index.ts +++ b/apps/backend/src/application/usecases/index.ts @@ -1,2 +1,3 @@ export * from './accounts'; export * from './auth'; +export * from './commodities'; diff --git a/apps/backend/src/db/createTestUser.ts b/apps/backend/src/db/createTestUser.ts index ac35d353..95d0db01 100644 --- a/apps/backend/src/db/createTestUser.ts +++ b/apps/backend/src/db/createTestUser.ts @@ -43,13 +43,12 @@ export const createCommodity = ( symbol = null, } = params; - return Commodity.create( - user, - Name.create(name), - CommodityCode.create(code), + return Commodity.create(user, { + code: CommodityCode.create(code).valueOf(), + name: Name.create(name).valueOf(), precision, symbol, - ); + }); }; export const createAccount = ( diff --git a/apps/backend/src/db/test-utils/testEntityBuilder.ts b/apps/backend/src/db/test-utils/testEntityBuilder.ts index 815fa42b..b331d63e 100644 --- a/apps/backend/src/db/test-utils/testEntityBuilder.ts +++ b/apps/backend/src/db/test-utils/testEntityBuilder.ts @@ -154,11 +154,10 @@ export class TransactionBuilder { currencyCodes.forEach((commodityCode) => { const code = CommodityCode.create(commodityCode); - const commodity = Commodity.create( - this.user, - Name.create(`Commodity ${commodityCode}`), - code, - ); + const commodity = Commodity.create(this.user, { + code: code.valueOf(), + name: `Commodity ${commodityCode}`, + }); const account = Account.create(this.user, { commodityId: commodity.getId(), diff --git a/apps/backend/src/domain/accounts/account.test.ts b/apps/backend/src/domain/accounts/account.test.ts index 29e1c808..b4fd40a3 100644 --- a/apps/backend/src/domain/accounts/account.test.ts +++ b/apps/backend/src/domain/accounts/account.test.ts @@ -26,13 +26,12 @@ describe('Account Domain Entity', () => { beforeAll(async () => { user = await createUser(); - commodity = Commodity.create( - user, - Name.create('commodity-name'), - CommodityCode.create('COM'), - 2, - 'C', - ); + commodity = Commodity.create(user, { + code: CommodityCode.create('COM').valueOf(), + name: 'commodity-name', + precision: 2, + symbol: 'C', + }); userId = user.getId(); }); diff --git a/apps/backend/src/domain/commodities/commodity.entity.ts b/apps/backend/src/domain/commodities/commodity.entity.ts index cc300d1f..0b5ae208 100644 --- a/apps/backend/src/domain/commodities/commodity.entity.ts +++ b/apps/backend/src/domain/commodities/commodity.entity.ts @@ -17,7 +17,14 @@ import { DeletedEntityOperationError } from '../domain.errors'; import { User } from '../users/user.entity'; import { parseCommodityPrecision, parseCommoditySymbol } from './helpers'; -import type { CommoditySnapshot, CommodityUpdateProps } from './types'; +import type { + CommoditySnapshot, + CommodityUpdateProps, + CreateCommodityProps, +} from './types'; + +const DEFAULT_PRECISION = 2; +const DEFAULT_SYMBOL = null; export class Commodity { static readonly entityType = 'commodity'; @@ -32,13 +39,7 @@ export class Commodity { private precision: CommodityPrecisionNumber, ) {} - static create( - user: User, - name: Name, - code: CommodityCode, - precision = 2, - symbol: string | null = null, - ): Commodity { + static create(user: User, props: CreateCommodityProps): Commodity { const identity = EntityIdentity.create(); const softDelete = SoftDelete.create(); const timestamps = EntityTimestamps.create(); @@ -48,17 +49,21 @@ export class Commodity { identity.getId(), ); - const parsedCommoditySymbol = parseCommoditySymbol(symbol); - const parsedCommodityPrecision = parseCommodityPrecision(precision); + const parsedCommoditySymbol = parseCommoditySymbol( + props.symbol ?? DEFAULT_SYMBOL, + ); + const parsedCommodityPrecision = parseCommodityPrecision( + props.precision ?? DEFAULT_PRECISION, + ); return new Commodity( identity, timestamps, softDelete, ownership, - code, + CommodityCode.create(props.code), parsedCommoditySymbol, - name, + Name.create(props.name), parsedCommodityPrecision, // use the provided precision value ); } diff --git a/apps/backend/src/domain/commodities/commodity.test.ts b/apps/backend/src/domain/commodities/commodity.test.ts index 8654c82d..d46dbdee 100644 --- a/apps/backend/src/domain/commodities/commodity.test.ts +++ b/apps/backend/src/domain/commodities/commodity.test.ts @@ -10,7 +10,7 @@ import { import { User } from '../users/user.entity'; import { Commodity } from './commodity.entity'; -import { CommoditySnapshot } from './types'; +import { CommoditySnapshot, CreateCommodityProps } from './types'; describe('Commodity Domain Entity', () => { let user: User; @@ -21,6 +21,13 @@ describe('Commodity Domain Entity', () => { const commodityName = Name.create('Commodity Name'); const commodityPrecision = 2; + const commodityCreateProps: CreateCommodityProps = { + code: commodityCode.valueOf(), + name: commodityName.valueOf(), + precision: commodityPrecision, + symbol: commoditySymbol, + }; + beforeAll(async () => { user = await createUser(); userId = user.getId(); @@ -32,13 +39,7 @@ describe('Commodity Domain Entity', () => { describe('create method', () => { it('should create commodity with valid data', () => { - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); expect(commodity).toBeInstanceOf(Commodity); expect(commodity.getId()).toBeDefined(); @@ -49,7 +50,10 @@ describe('Commodity Domain Entity', () => { }); it('should create commodity with default precision and null symbol', () => { - const commodity = Commodity.create(user, commodityName, commodityCode); + const commodity = Commodity.create(user, { + code: commodityCode.valueOf(), + name: commodityName.valueOf(), + }); expect(commodity).toBeInstanceOf(Commodity); expect(commodity.getId()).toBeDefined(); @@ -61,12 +65,11 @@ describe('Commodity Domain Entity', () => { it('should create commodity with specified precision and null symbol', () => { const specifiedPrecision = 4; - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - specifiedPrecision, - ); + const commodity = Commodity.create(user, { + code: commodityCode.valueOf(), + name: commodityName.valueOf(), + precision: specifiedPrecision, + }); expect(commodity).toBeInstanceOf(Commodity); expect(commodity.getId()).toBeDefined(); @@ -80,7 +83,10 @@ describe('Commodity Domain Entity', () => { const invalidPrecision = -1; expect(() => { - Commodity.create(user, commodityName, commodityCode, invalidPrecision); + Commodity.create(user, { + ...commodityCreateProps, + precision: invalidPrecision, + }); }).toThrowError(InvalidCommodityPrecisionError); }); @@ -88,13 +94,10 @@ describe('Commodity Domain Entity', () => { const invalidSymbol = 'INVALID_SYMBOL'; expect(() => { - Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - invalidSymbol, - ); + Commodity.create(user, { + ...commodityCreateProps, + symbol: invalidSymbol, + }); }).toThrowError(InvalidCommoditySymbolError); }); }); @@ -176,13 +179,7 @@ describe('Commodity Domain Entity', () => { describe('commodity management', () => { describe('markAsDeleted and isDeleted methods', () => { it('should mark commodity as deleted and check deletion status', () => { - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); expect(commodity.isDeleted()).toBe(false); @@ -192,13 +189,7 @@ describe('Commodity Domain Entity', () => { }); it('should not allow updates on a deleted commodity', () => { - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); commodity.markAsDeleted(); @@ -210,13 +201,7 @@ describe('Commodity Domain Entity', () => { }); it('should not allow deleting an already deleted commodity', () => { - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); commodity.markAsDeleted(); @@ -249,13 +234,7 @@ describe('Commodity Domain Entity', () => { vi.setSystemTime(new Date(timestampBeforeDeletionValue)); - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); const commoditySnapshotBeforeDeletion = commodity.toSnapshot(); @@ -286,13 +265,7 @@ describe('Commodity Domain Entity', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z')); - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); const before = commodity.toSnapshot(); @@ -318,13 +291,7 @@ describe('Commodity Domain Entity', () => { }); it('should throw error when updating commodity with invalid symbol', () => { - const commodity = Commodity.create( - user, - commodityName, - commodityCode, - commodityPrecision, - commoditySymbol, - ); + const commodity = Commodity.create(user, commodityCreateProps); expect(() => { commodity.update({ @@ -334,7 +301,7 @@ describe('Commodity Domain Entity', () => { }); it('should not update timestamp for empty update', () => { - const commodity = Commodity.create(user, commodityName, commodityCode); + const commodity = Commodity.create(user, commodityCreateProps); const snapshot = commodity.toSnapshot(); commodity.update({}); diff --git a/apps/backend/src/domain/commodities/index.ts b/apps/backend/src/domain/commodities/index.ts index eada130c..366221c8 100644 --- a/apps/backend/src/domain/commodities/index.ts +++ b/apps/backend/src/domain/commodities/index.ts @@ -1,2 +1,6 @@ export { Commodity } from './commodity.entity'; -export { CommoditySnapshot } from './types'; +export { + CommoditySnapshot, + CommodityUpdateProps, + CreateCommodityProps, +} from './types'; diff --git a/apps/backend/src/domain/commodities/types.ts b/apps/backend/src/domain/commodities/types.ts index 1af354a1..de76f723 100644 --- a/apps/backend/src/domain/commodities/types.ts +++ b/apps/backend/src/domain/commodities/types.ts @@ -4,6 +4,7 @@ import { IsoDatetimeString, UUID, CommodityPrecisionNumber, + NameString, } from '@ledgerly/shared/types'; export type CommoditySnapshot = { @@ -14,12 +15,19 @@ export type CommoditySnapshot = { symbol: CommoditySymbolString | null; updatedAt: IsoDatetimeString; userId: UUID; - name: string; + name: NameString; precision: CommodityPrecisionNumber; }; export type CommodityUpdateProps = Partial<{ - code: string; - symbol: string | null; - name: string; + code: CommodityCodeString; + symbol: CommoditySymbolString | null; + name: NameString; }>; + +export type CreateCommodityProps = { + code: CommodityCodeString; + name: NameString; + precision?: CommodityPrecisionNumber; + symbol?: CommoditySymbolString | null; +}; diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 8a49c02f..22cad758 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -145,10 +145,8 @@ describe('CommodityRepository', () => { userId: user.id, }; - const createdCommodity = await commodityRepository.create( - user.id, - newCommodityData, - ); + const createdCommodity = + await commodityRepository.create(newCommodityData); expect(createdCommodity).toMatchObject(newCommodityData); @@ -176,7 +174,7 @@ describe('CommodityRepository', () => { }; await expect( - commodityRepository.create(user.id, newCommodityData), + commodityRepository.create(newCommodityData), ).rejects.toThrowError( new RecordAlreadyExistsError({ context: { @@ -205,10 +203,8 @@ describe('CommodityRepository', () => { userId: anotherUser.id, }; - const createdCommodityForAnotherUser = await commodityRepository.create( - anotherUser.id, - newCommodityData, - ); + const createdCommodityForAnotherUser = + await commodityRepository.create(newCommodityData); expect(createdCommodityForAnotherUser).toMatchObject(newCommodityData); @@ -219,40 +215,6 @@ describe('CommodityRepository', () => { expect(retrievedCommodity).toEqual(createdCommodityForAnotherUser); }); - - it('should associate a created commodity with the userId argument', async () => { - const anotherUser = await testDB.createUser(); - - const newCommodityData: CommoditySnapshot = { - code: CommodityCode.create('OWN').valueOf(), - createdAt: Timestamp.create().valueOf(), - id: Id.create().valueOf(), - isTombstone: false, - name: Name.create('Owned Commodity').valueOf(), - precision: 2, - symbol: 'O', - updatedAt: Timestamp.create().valueOf(), - userId: anotherUser.id, - }; - - const createdCommodity = await commodityRepository.create( - user.id, - newCommodityData, - ); - - expect(createdCommodity.userId).toBe(user.id); - - await expect( - commodityRepository.getById(anotherUser.id, createdCommodity.id), - ).rejects.toThrowError(RepositoryNotFoundError); - - await expect( - commodityRepository.getById(user.id, createdCommodity.id), - ).resolves.toMatchObject({ - id: newCommodityData.id, - userId: user.id, - }); - }); }); describe('update', () => { diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index eb476d8f..01d98b6d 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -59,16 +59,11 @@ export class CommodityRepository }, 'Failed to fetch commodities'); } - create( - userId: UUID, - commodity: CommoditySnapshot, - ): Promise { + create(commodity: CommoditySnapshot): Promise { return this.executeDatabaseOperation( async () => { - const commodityRow = CommodityPersistenceMapper.toDBRowFromSnapshot({ - ...commodity, - userId, - }); + const commodityRow = + CommodityPersistenceMapper.toDBRowFromSnapshot(commodity); const createdCommodity = await this.db .insert(commoditiesTable) diff --git a/packages/shared/src/routes/index.ts b/packages/shared/src/routes/index.ts index 9a90c9bd..c24f5d90 100644 --- a/packages/shared/src/routes/index.ts +++ b/packages/shared/src/routes/index.ts @@ -1,7 +1,7 @@ export const ROUTES = { accounts: "/accounts", auth: "/auth", - entries: "/entries", + commodities: "/commodities", transactions: "/transactions", user: "/user", }; diff --git a/packages/shared/src/types/account.ts b/packages/shared/src/types/account.ts index 1cc4f117..80993e68 100644 --- a/packages/shared/src/types/account.ts +++ b/packages/shared/src/types/account.ts @@ -1,4 +1,4 @@ -import { IsoDatetimeString, AmountString, UUID } from "./types"; +import { IsoDatetimeString, AmountString, UUID, NameString } from "./types"; export type AccountTypeValue = | "asset" @@ -8,21 +8,6 @@ export type AccountTypeValue = | "currencyTrading" | "expense"; -export type AccountDomain = { - commodityId: UUID; - createdAt: IsoDatetimeString; - currentClearedBalanceLocal: AmountString; - description: string; - id: UUID; - initialBalance: AmountString; - isSystem: boolean; - isTombstone: boolean; - name: string; - type: AccountTypeValue; - updatedAt: IsoDatetimeString; - userId: UUID; -}; - export type AccountCreateDTO = { commodityId: UUID; description: string; @@ -35,4 +20,17 @@ export type AccountUpdateDTO = Partial< Pick >; -export type AccountResponseDTO = AccountDomain; +export type AccountResponseDTO = { + commodityId: UUID; + createdAt: IsoDatetimeString; + currentClearedBalanceLocal: AmountString; + description: string; + id: UUID; + initialBalance: AmountString; + isSystem: boolean; + isTombstone: boolean; + name: NameString; + type: AccountTypeValue; + updatedAt: IsoDatetimeString; + userId: UUID; +}; diff --git a/packages/shared/src/types/commodity.ts b/packages/shared/src/types/commodity.ts new file mode 100644 index 00000000..83b3575a --- /dev/null +++ b/packages/shared/src/types/commodity.ts @@ -0,0 +1,27 @@ +import { + CommodityCodeString, + CommodityPrecisionNumber, + CommoditySymbolString, + IsoDatetimeString, + NameString, + UUID, +} from "./types"; + +export type CommodityCreateDTO = { + code: CommodityCodeString; + name: NameString; + precision?: CommodityPrecisionNumber; + symbol?: CommoditySymbolString | null; +}; + +export type CommodityResponseDTO = { + code: CommodityCodeString; + createdAt: IsoDatetimeString; + id: UUID; + isTombstone: boolean; + name: NameString; + precision: CommodityPrecisionNumber; + symbol: CommoditySymbolString | null; + updatedAt: IsoDatetimeString; + userId: UUID; +}; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index a4fb3e21..7f364b89 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -5,3 +5,4 @@ export * from "./types"; export * from "./error-contract"; export * from "./pagination"; export * from "./transaction"; +export * from "./commodity"; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 686aab6b..387d412a 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -9,16 +9,15 @@ import { commodityCode, commodityPrecision, commoditySymbol, + requiredText, } from "../validation/baseValidations"; export type IsoDatetimeString = z.infer; export type IsoDateString = z.infer; export type Sha256String = z.infer; - export type UUID = z.infer; - export type AmountString = z.infer; - export type CommodityCodeString = z.infer; export type CommodityPrecisionNumber = z.infer; export type CommoditySymbolString = z.infer; +export type NameString = z.infer; From 9f30fca41ea4e6da6a9355a72372a32228b185f9 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Sun, 26 Jul 2026 10:03:28 +0300 Subject: [PATCH 22/30] LED-110 feat(commodities): implement CRUD operations for commodities --- .../application/mappers/commodity.mapper.ts | 10 + .../application/shared/ensureOwnedSnapshot.ts | 38 +++ .../__tests__/archiveCommodity.test.ts | 137 +++++++++++ .../__tests__/getAllCommodities.test.ts | 79 +++++++ .../__tests__/getCommodityById.test.ts | 65 ++++++ .../__tests__/updateCommodity.test.ts | 155 +++++++++++++ .../usecases/commodities/archiveCommodity.ts | 35 +++ .../usecases/commodities/getAllCommodities.ts | 20 ++ .../usecases/commodities/getCommodityById.ts | 19 ++ .../application/usecases/commodities/index.ts | 4 + .../usecases/commodities/updateCommodity.ts | 43 ++++ .../backend/src/application/usecases/index.ts | 1 + apps/backend/src/db/test-db.ts | 10 + apps/backend/src/di/container.ts | 59 ++++- apps/backend/src/di/types.ts | 37 ++- apps/backend/src/domain/commodities/types.ts | 8 +- .../commodities/commodity.repository.test.ts | 9 +- .../db/commodities/commodity.repository.ts | 6 +- .../{accounts.routes.ts => account.routes.ts} | 0 .../src/presentation/http/accounts/index.ts | 2 +- .../commodities/commodity.controller.test.ts | 218 ++++++++++++++++++ .../http/commodities/commodity.controller.ts | 66 ++++++ .../commodities/commodity.integration.test.ts | 118 ++++++++++ .../http/commodities/commodity.routes.ts | 44 ++++ .../presentation/http/commodities/index.ts | 2 + apps/backend/src/presentation/http/index.ts | 1 + packages/shared/src/types/account.ts | 4 +- packages/shared/src/types/commodity.ts | 10 +- packages/shared/src/types/types.ts | 2 +- packages/shared/src/validation/accounts.ts | 6 +- .../shared/src/validation/baseValidations.ts | 13 +- packages/shared/src/validation/commodities.ts | 23 ++ packages/shared/src/validation/index.ts | 1 + .../shared/src/validation/transactions.ts | 15 +- packages/shared/src/validation/users.ts | 8 +- 35 files changed, 1208 insertions(+), 60 deletions(-) create mode 100644 apps/backend/src/application/shared/ensureOwnedSnapshot.ts create mode 100644 apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts create mode 100644 apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts create mode 100644 apps/backend/src/application/usecases/commodities/__tests__/getCommodityById.test.ts create mode 100644 apps/backend/src/application/usecases/commodities/__tests__/updateCommodity.test.ts create mode 100644 apps/backend/src/application/usecases/commodities/archiveCommodity.ts create mode 100644 apps/backend/src/application/usecases/commodities/getAllCommodities.ts create mode 100644 apps/backend/src/application/usecases/commodities/getCommodityById.ts create mode 100644 apps/backend/src/application/usecases/commodities/updateCommodity.ts rename apps/backend/src/presentation/http/accounts/{accounts.routes.ts => account.routes.ts} (100%) create mode 100644 apps/backend/src/presentation/http/commodities/commodity.controller.test.ts create mode 100644 apps/backend/src/presentation/http/commodities/commodity.controller.ts create mode 100644 apps/backend/src/presentation/http/commodities/commodity.integration.test.ts create mode 100644 apps/backend/src/presentation/http/commodities/commodity.routes.ts create mode 100644 apps/backend/src/presentation/http/commodities/index.ts create mode 100644 packages/shared/src/validation/commodities.ts diff --git a/apps/backend/src/application/mappers/commodity.mapper.ts b/apps/backend/src/application/mappers/commodity.mapper.ts index bf871df0..63bf9384 100644 --- a/apps/backend/src/application/mappers/commodity.mapper.ts +++ b/apps/backend/src/application/mappers/commodity.mapper.ts @@ -1,9 +1,11 @@ import { CommodityCreateDTO, CommodityResponseDTO, + CommodityUpdateDTO, } from '@ledgerly/shared/types'; import { CommoditySnapshot, + CommodityUpdateProps, CreateCommodityProps, } from 'src/domain/commodities'; @@ -34,4 +36,12 @@ export class CommodityMapper { symbol: props.symbol, }; } + + static toUpdateProps(props: CommodityUpdateDTO): CommodityUpdateProps { + return { + code: props.code, + name: props.name, + symbol: props.symbol, + }; + } } diff --git a/apps/backend/src/application/shared/ensureOwnedSnapshot.ts b/apps/backend/src/application/shared/ensureOwnedSnapshot.ts new file mode 100644 index 00000000..15bd0f5c --- /dev/null +++ b/apps/backend/src/application/shared/ensureOwnedSnapshot.ts @@ -0,0 +1,38 @@ +import { UUID } from '@ledgerly/shared/types'; +import { + EntityNotFoundError, + UnauthorizedAccessError, +} from 'src/application/application.errors'; +import { User } from 'src/domain'; + +export type EnsureOwnedSnapshotOptions = { + entityId: UUID; + entityType: string; + getOwnerId: (entity: T) => UUID; + load: (userId: UUID, entityId: UUID) => Promise; + user: User; +}; + +export type EnsureOwnedSnapshotFn = ( + options: EnsureOwnedSnapshotOptions, +) => Promise; + +export const ensureOwnedSnapshot: EnsureOwnedSnapshotFn = async ({ + entityId, + entityType, + getOwnerId, + load, + user, +}: EnsureOwnedSnapshotOptions): Promise => { + const entity = await load(user.getId().valueOf(), entityId); + + if (!entity) { + throw new EntityNotFoundError({ entityId, entityType }); + } + + if (!user.verifyOwnership(getOwnerId(entity))) { + throw new UnauthorizedAccessError({ entityId, entityType }); + } + + return entity; +}; diff --git a/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts new file mode 100644 index 00000000..b01989b4 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts @@ -0,0 +1,137 @@ +import { CommodityRepositoryInterface, CommodityMapper } from 'src/application'; +import { + EntityNotFoundError, + UnauthorizedAccessError, +} from 'src/application/application.errors'; +import { ensureOwnedSnapshot } from 'src/application/shared/ensureOwnedSnapshot'; +import { createCommodity } from 'src/db/createTestUser'; +import { Commodity } from 'src/domain/commodities/commodity.entity'; +import { Id, Timestamp } from 'src/domain/domain-core/'; +import { User } from 'src/domain/users/user.entity'; +import { createUser } from 'src/testing/helpers'; +import { describe, vi, beforeAll, beforeEach, expect, it } from 'vitest'; + +import { ArchiveCommodityUseCase } from '../archiveCommodity'; + +describe('ArchiveCommodityUseCase', () => { + let user: User; + + const commodityRepository = { + delete: vi.fn(), + getById: vi.fn(), + }; + + const archiveCommodityUseCase = new ArchiveCommodityUseCase( + commodityRepository as unknown as CommodityRepositoryInterface, + ensureOwnedSnapshot, + ); + + beforeAll(async () => { + user = await createUser(); + }); + + beforeEach(() => { + vi.useRealTimers(); + commodityRepository.delete.mockClear(); + commodityRepository.getById.mockClear(); + }); + + describe('execute', () => { + it('should archive a commodity successfully', async () => { + vi.useFakeTimers(); + + const timestampBeforeUpdatingValue = '2024-01-01T00:00:00.000Z'; + const timestampDuringUpdatingValue = '2025-01-01T00:00:01.000Z'; + + vi.setSystemTime(new Date(timestampBeforeUpdatingValue)); + + // Arrange + const commodity = createCommodity(user, { + code: 'TEST', + name: 'Test Commodity', + precision: 2, + symbol: 'T', + }); + + const commodityId = commodity.getId().valueOf(); + const userid = user.getId().valueOf(); + + const archivedCommodityData = { + ...commodity.toSnapshot(), + isTombstone: true, + timestamps: { + createdAt: commodity.getCreatedAt().valueOf(), + updatedAt: Timestamp.restore(timestampDuringUpdatingValue).valueOf(), + }, + }; + + commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); + commodityRepository.delete.mockResolvedValue(archivedCommodityData); + + vi.setSystemTime(new Date(timestampDuringUpdatingValue)); + + const expectedResult = CommodityMapper.toResponseDTOFromSnapshot( + archivedCommodityData, + ); + + // Act + const result = await archiveCommodityUseCase.execute(user, commodityId); + + // Assert + expect(commodityRepository.getById).toHaveBeenCalledWith( + userid, + commodityId, + ); + + expect(commodityRepository.delete).toHaveBeenCalledWith( + userid, + commodityId, + { updatedAt: archivedCommodityData.timestamps.updatedAt }, + ); + + expect(result).toMatchObject(expectedResult); + }); + + it('should throw an error if the commodity does not exist', async () => { + // Arrange + const nonExistentCommodityId = Id.create().valueOf(); + + commodityRepository.getById.mockResolvedValue(null); + + // Act & Assert + await expect( + archiveCommodityUseCase.execute(user, nonExistentCommodityId), + ).rejects.toThrowError( + new EntityNotFoundError({ + entityId: nonExistentCommodityId, + entityType: Commodity.entityType, + }), + ); + }); + + it('should throw an error if the commodity does not belong to the user', async () => { + // Arrange + const otherUser = await createUser(); + const commodity = createCommodity(otherUser, { + code: 'TEST', + name: 'Test Commodity', + precision: 2, + symbol: 'T', + }); + + const commodityId = commodity.getId().valueOf(); + + commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); + + // Act & Assert + await expect( + archiveCommodityUseCase.execute(user, commodityId), + ).rejects.toThrowError( + new UnauthorizedAccessError({ + entityId: commodityId, + entityType: Commodity.entityType, + }), + ); + }); + }); +}); diff --git a/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts new file mode 100644 index 00000000..645677af --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts @@ -0,0 +1,79 @@ +import { CommodityRepositoryInterface } from 'src/application'; +import { User } from 'src/domain/users/user.entity'; +import { createUser } from 'src/testing/helpers'; +import { describe, vi, beforeAll, beforeEach, expect, it } from 'vitest'; + +import { GetAllCommoditiesUseCase } from '../getAllCommodities'; + +describe('GetAllCommoditiesUseCase', () => { + let user: User; + + const commodityRepository = { + getAll: vi.fn(), + }; + + const getAllCommoditiesUseCase = new GetAllCommoditiesUseCase( + commodityRepository as unknown as CommodityRepositoryInterface, + ); + + beforeAll(async () => { + user = await createUser(); + }); + + beforeEach(() => { + commodityRepository.getAll.mockClear(); + }); + + describe('execute', () => { + it('should return an array of commodities for the user', async () => { + // Arrange + const mockCommodities = [ + { + code: 'USD', + createdAt: new Date(), + id: '1', + isTombstone: false, + name: 'US Dollar', + precision: 2, + symbol: '$', + updatedAt: new Date(), + userId: user.getId().valueOf(), + }, + { + code: 'EUR', + createdAt: new Date(), + id: '2', + isTombstone: false, + name: 'Euro', + precision: 2, + symbol: '€', + updatedAt: new Date(), + userId: user.getId().valueOf(), + }, + ]; + + commodityRepository.getAll.mockResolvedValue(mockCommodities); + + // Act + const result = await getAllCommoditiesUseCase.execute(user); + + // Assert + expect(commodityRepository.getAll).toHaveBeenCalledWith( + user.getId().valueOf(), + ); + expect(result).toEqual( + mockCommodities.map((commodity) => ({ + code: commodity.code, + createdAt: commodity.createdAt, + id: commodity.id, + isTombstone: commodity.isTombstone, + name: commodity.name, + precision: commodity.precision, + symbol: commodity.symbol, + updatedAt: commodity.updatedAt, + userId: commodity.userId, + })), + ); + }); + }); +}); diff --git a/apps/backend/src/application/usecases/commodities/__tests__/getCommodityById.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/getCommodityById.test.ts new file mode 100644 index 00000000..4407e750 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/__tests__/getCommodityById.test.ts @@ -0,0 +1,65 @@ +import { CommodityRepositoryInterface } from 'src/application'; +import { Id } from 'src/domain/domain-core'; +import { User } from 'src/domain/users/user.entity'; +import { createUser } from 'src/testing/helpers'; +import { describe, vi, beforeAll, beforeEach, expect, it } from 'vitest'; + +import { GetCommodityByIdUseCase } from '../getCommodityById'; + +describe('GetCommodityByIdUseCase', () => { + let user: User; + + const commodityRepository = { + getById: vi.fn(), + }; + + const getCommodityByIdUseCase = new GetCommodityByIdUseCase( + commodityRepository as unknown as CommodityRepositoryInterface, + ); + + beforeAll(async () => { + user = await createUser(); + }); + + beforeEach(() => { + commodityRepository.getById.mockClear(); + }); + + describe('execute', () => { + it('should return a commodity by its ID for the user', async () => { + // Arrange + const commodityId = Id.create().valueOf(); + + const mockCommodity = { + code: 'USD', + createdAt: new Date(), + id: commodityId, + isTombstone: false, + name: 'US Dollar', + precision: 2, + symbol: '$', + updatedAt: new Date(), + userId: user.getId().valueOf(), + }; + + commodityRepository.getById.mockResolvedValue(mockCommodity); + + // Act + const result = await getCommodityByIdUseCase.execute(user, commodityId); + + // Assert + expect(commodityRepository.getById).toHaveBeenCalledWith( + user.getId().valueOf(), + commodityId, + ); + expect(result).toMatchObject({ + code: mockCommodity.code, + isTombstone: mockCommodity.isTombstone, + name: mockCommodity.name, + precision: mockCommodity.precision, + symbol: mockCommodity.symbol, + userId: mockCommodity.userId, + }); + }); + }); +}); diff --git a/apps/backend/src/application/usecases/commodities/__tests__/updateCommodity.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/updateCommodity.test.ts new file mode 100644 index 00000000..e4c9a8fa --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/__tests__/updateCommodity.test.ts @@ -0,0 +1,155 @@ +import { CommodityResponseDTO } from '@ledgerly/shared/types'; +import { CommodityRepositoryInterface } from 'src/application'; +import { + EntityNotFoundError, + UnauthorizedAccessError, +} from 'src/application/application.errors'; +import { ensureOwnedSnapshot } from 'src/application/shared/ensureOwnedSnapshot'; +import { createCommodity } from 'src/db/createTestUser'; +import { Commodity } from 'src/domain/commodities/commodity.entity'; +import { Id, Timestamp } from 'src/domain/domain-core/'; +import { User } from 'src/domain/users/user.entity'; +import { createUser } from 'src/testing/helpers'; +import { describe, vi, beforeAll, beforeEach, expect, it } from 'vitest'; + +import { UpdateCommodityUseCase } from '../updateCommodity'; + +describe('UpdateCommodityUseCase', () => { + let user: User; + + const commodityRepository = { + getById: vi.fn(), + update: vi.fn(), + }; + + const updateCommodityUseCase = new UpdateCommodityUseCase( + commodityRepository as unknown as CommodityRepositoryInterface, + ensureOwnedSnapshot, + ); + + beforeAll(async () => { + user = await createUser(); + }); + + beforeEach(() => { + vi.useRealTimers(); + commodityRepository.update.mockClear(); + commodityRepository.getById.mockClear(); + }); + + describe('execute', () => { + it('should update a commodity successfully', async () => { + vi.useFakeTimers(); + + const timestampBeforeUpdatingValue = '2024-01-01T00:00:00.000Z'; + const timestampDuringUpdatingValue = '2025-01-01T00:00:01.000Z'; + + vi.setSystemTime(new Date(timestampBeforeUpdatingValue)); + + // Arrange + const commodity = createCommodity(user, { + code: 'TEST', + name: 'Test Commodity', + precision: 2, + symbol: 'T', + }); + + const commodityId = commodity.getId().valueOf(); + const userid = user.getId().valueOf(); + + const updateData = { + name: 'Updated Commodity', + symbol: 'U', + }; + + const updatedCommodityData = { + ...commodity.toSnapshot(), + ...updateData, + updatedAt: Timestamp.restore(timestampDuringUpdatingValue).valueOf(), + }; + + commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); + + commodityRepository.update.mockResolvedValue(updatedCommodityData); + + vi.setSystemTime(new Date(timestampDuringUpdatingValue)); + + const updatedCommoditySnapshot: CommodityResponseDTO = { + ...updatedCommodityData, + updatedAt: Timestamp.restore(timestampDuringUpdatingValue).valueOf(), + }; + + // Act + const result = await updateCommodityUseCase.execute( + user, + commodityId, + updateData, + ); + + // Assert + expect(commodityRepository.getById).toHaveBeenCalledWith( + userid, + commodityId, + ); + + expect(commodityRepository.update).toHaveBeenCalledWith( + userid, + commodityId, + { + ...updatedCommodityData, + updatedAt: Timestamp.restore(timestampDuringUpdatingValue).valueOf(), + }, + ); + + expect(result).toEqual(updatedCommoditySnapshot); + }); + + it('should throw an error if the commodity does not exist', async () => { + // Arrange + const nonExistentCommodityId = Id.create().valueOf(); + + commodityRepository.getById.mockResolvedValue(null); + + // Act & Assert + await expect( + updateCommodityUseCase.execute(user, nonExistentCommodityId, { + name: 'Updated Commodity', + symbol: 'U', + }), + ).rejects.toThrowError( + new EntityNotFoundError({ + entityId: nonExistentCommodityId, + entityType: Commodity.entityType, + }), + ); + }); + + it('should throw an error if the commodity does not belong to the user', async () => { + // Arrange + const otherUser = await createUser(); + const commodity = createCommodity(otherUser, { + code: 'TEST', + name: 'Test Commodity', + precision: 2, + symbol: 'T', + }); + + const commodityId = commodity.getId().valueOf(); + + commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); + + // Act & Assert + await expect( + updateCommodityUseCase.execute(user, commodityId, { + name: 'Updated Commodity', + symbol: 'U', + }), + ).rejects.toThrowError( + new UnauthorizedAccessError({ + entityId: commodityId, + entityType: Commodity.entityType, + }), + ); + }); + }); +}); diff --git a/apps/backend/src/application/usecases/commodities/archiveCommodity.ts b/apps/backend/src/application/usecases/commodities/archiveCommodity.ts new file mode 100644 index 00000000..b960c116 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/archiveCommodity.ts @@ -0,0 +1,35 @@ +import { CommodityResponseDTO, UUID } from '@ledgerly/shared/types'; +import { CommodityMapper } from 'src/application'; +import { CommodityRepositoryInterface } from 'src/application/interfaces'; +import { EnsureOwnedSnapshotFn } from 'src/application/shared/ensureOwnedSnapshot'; +import { Commodity, CommoditySnapshot } from 'src/domain/commodities'; +import { User } from 'src/domain/users/user.entity'; + +export class ArchiveCommodityUseCase { + constructor( + protected readonly commodityRepository: CommodityRepositoryInterface, + protected readonly ensureOwnedSnapshot: EnsureOwnedSnapshotFn, + ) {} + + async execute(user: User, commodityId: UUID): Promise { + const commodityData = await this.ensureOwnedSnapshot({ + entityId: commodityId, + entityType: Commodity.entityType, + getOwnerId: (commodity) => commodity.userId, + load: this.commodityRepository.getById.bind(this.commodityRepository), + user, + }); + + const commodity = Commodity.restore(commodityData); + + commodity.markAsDeleted(); + + const updatedCommodity = await this.commodityRepository.delete( + user.getId().valueOf(), + commodityId, + { updatedAt: commodity.getUpdatedAt().valueOf() }, + ); + + return CommodityMapper.toResponseDTOFromSnapshot(updatedCommodity); + } +} diff --git a/apps/backend/src/application/usecases/commodities/getAllCommodities.ts b/apps/backend/src/application/usecases/commodities/getAllCommodities.ts new file mode 100644 index 00000000..c246a780 --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/getAllCommodities.ts @@ -0,0 +1,20 @@ +import { CommodityResponseDTO } from '@ledgerly/shared/types'; +import { CommodityMapper } from 'src/application'; +import { CommodityRepositoryInterface } from 'src/application/interfaces'; +import { User } from 'src/domain/users/user.entity'; + +export class GetAllCommoditiesUseCase { + constructor( + protected readonly commodityRepository: CommodityRepositoryInterface, + ) {} + + async execute(user: User): Promise { + const commoditiesSnapshots = await this.commodityRepository.getAll( + user.getId().valueOf(), + ); + + return commoditiesSnapshots.map((commoditySnapshot) => + CommodityMapper.toResponseDTOFromSnapshot(commoditySnapshot), + ); + } +} diff --git a/apps/backend/src/application/usecases/commodities/getCommodityById.ts b/apps/backend/src/application/usecases/commodities/getCommodityById.ts new file mode 100644 index 00000000..7c02f65e --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/getCommodityById.ts @@ -0,0 +1,19 @@ +import { CommodityResponseDTO, UUID } from '@ledgerly/shared/types'; +import { CommodityMapper } from 'src/application'; +import { CommodityRepositoryInterface } from 'src/application/interfaces'; +import { User } from 'src/domain/users/user.entity'; + +export class GetCommodityByIdUseCase { + constructor( + protected readonly commodityRepository: CommodityRepositoryInterface, + ) {} + + async execute(user: User, commodityId: UUID): Promise { + const commodity = await this.commodityRepository.getById( + user.getId().valueOf(), + commodityId, + ); + + return CommodityMapper.toResponseDTOFromSnapshot(commodity); + } +} diff --git a/apps/backend/src/application/usecases/commodities/index.ts b/apps/backend/src/application/usecases/commodities/index.ts index 064dd0f9..68430416 100644 --- a/apps/backend/src/application/usecases/commodities/index.ts +++ b/apps/backend/src/application/usecases/commodities/index.ts @@ -1 +1,5 @@ export { CreateCommodityUseCase } from './createCommodity'; +export { UpdateCommodityUseCase } from './updateCommodity'; +export { GetAllCommoditiesUseCase } from './getAllCommodities'; +export { GetCommodityByIdUseCase } from './getCommodityById'; +export { ArchiveCommodityUseCase } from './archiveCommodity'; diff --git a/apps/backend/src/application/usecases/commodities/updateCommodity.ts b/apps/backend/src/application/usecases/commodities/updateCommodity.ts new file mode 100644 index 00000000..82a46b0e --- /dev/null +++ b/apps/backend/src/application/usecases/commodities/updateCommodity.ts @@ -0,0 +1,43 @@ +import { + CommodityResponseDTO, + UUID, + CommodityUpdateDTO, +} from '@ledgerly/shared/types'; +import { CommodityMapper } from 'src/application'; +import { CommodityRepositoryInterface } from 'src/application/interfaces'; +import { EnsureOwnedSnapshotFn } from 'src/application/shared/ensureOwnedSnapshot'; +import { Commodity, CommoditySnapshot } from 'src/domain/commodities'; +import { User } from 'src/domain/users/user.entity'; + +export class UpdateCommodityUseCase { + constructor( + protected readonly commodityRepository: CommodityRepositoryInterface, + protected readonly ensureOwnedSnapshot: EnsureOwnedSnapshotFn, + ) {} + + async execute( + user: User, + commodityId: UUID, + data: CommodityUpdateDTO, + ): Promise { + const commodityData = await this.ensureOwnedSnapshot({ + entityId: commodityId, + entityType: Commodity.entityType, + getOwnerId: (commodity) => commodity.userId, + load: this.commodityRepository.getById.bind(this.commodityRepository), + user, + }); + + const commodity = Commodity.restore(commodityData); + + commodity.update(CommodityMapper.toUpdateProps(data)); + + const updatedCommodity = await this.commodityRepository.update( + user.getId().valueOf(), + commodityId, + commodity.toSnapshot(), + ); + + return CommodityMapper.toResponseDTOFromSnapshot(updatedCommodity); + } +} diff --git a/apps/backend/src/application/usecases/index.ts b/apps/backend/src/application/usecases/index.ts index 6f6e1a17..b1b71415 100644 --- a/apps/backend/src/application/usecases/index.ts +++ b/apps/backend/src/application/usecases/index.ts @@ -1,3 +1,4 @@ export * from './accounts'; export * from './auth'; export * from './commodities'; +export * from './transaction'; diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index 76adc731..4bc1a439 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -548,6 +548,16 @@ export class TestDB { return operations; }; + getCommodityById = async (commodityId: UUID) => { + const commodity = await this.db + .select() + .from(schema.commoditiesTable) + .where(sql`${schema.commoditiesTable.id} = ${commodityId}`) + .get(); + + return commodity ?? null; + }; + deleteData = async () => { await this.db.delete(transactionsTable); await this.db.delete(accountsTable); diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index 92f63f32..7d3b36d4 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -1,21 +1,25 @@ -import { LoginUserUseCase, RegisterUserUseCase } from 'src/application'; -import { TransactionContextLoader } from 'src/application/services/TransactionService'; -import { ensureEntityExistsAndOwned } from 'src/application/shared/ensureEntityExistsAndOwned'; import { + GetAllCommoditiesUseCase, + GetCommodityByIdUseCase, + LoginUserUseCase, + RegisterUserUseCase, CreateAccountUseCase, DeleteAccountUseCase, GetAccountByIdUseCase, GetAllAccountsUseCase, UpdateAccountUseCase, -} from 'src/application/usecases/accounts/'; -import {} from 'src/application/usecases/accounts/deleteAccount'; -import { - CreateTransactionUseCase, DeleteTransactionUseCase, GetAllTransactionsUseCase, GetTransactionByIdUseCase, UpdateTransactionUseCase, -} from 'src/application/usecases/transaction/'; + CreateTransactionUseCase, + UpdateCommodityUseCase, + ArchiveCommodityUseCase, + CreateCommodityUseCase, +} from 'src/application'; +import { TransactionContextLoader } from 'src/application/services/TransactionService'; +import { ensureEntityExistsAndOwned } from 'src/application/shared/ensureEntityExistsAndOwned'; +import { ensureOwnedSnapshot } from 'src/application/shared/ensureOwnedSnapshot'; import { DataBase } from 'src/db'; import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; import { @@ -30,6 +34,7 @@ import { CommodityRepository } from 'src/infrastructure/db'; import { AccountController, AuthController, + CommodityController, TransactionController, UserController, } from 'src/presentation/http'; @@ -113,6 +118,36 @@ export const createContainer = (db: DataBase): AppContainer => { ensureEntityExistsAndOwned, ); + const getCommodityByIdUseCase = new GetCommodityByIdUseCase( + commodityRepository, + ); + + const getAllCommoditiesUseCase = new GetAllCommoditiesUseCase( + commodityRepository, + ); + + const updateCommodityUseCase = new UpdateCommodityUseCase( + commodityRepository, + ensureOwnedSnapshot, + ); + + const archiveCommodityUseCase = new ArchiveCommodityUseCase( + commodityRepository, + ensureOwnedSnapshot, + ); + + const createCommodityUseCase = new CreateCommodityUseCase( + commodityRepository, + ); + + const commodityController = new CommodityController( + getCommodityByIdUseCase, + getAllCommoditiesUseCase, + createCommodityUseCase, + updateCommodityUseCase, + archiveCommodityUseCase, + ); + const useCases: AppContainer['useCases'] = { account: { archiveAccount: deleteAccountUseCase, @@ -125,6 +160,13 @@ export const createContainer = (db: DataBase): AppContainer => { loginUser: loginUserUseCase, registerUser: registerUserUseCase, }, + commodity: { + archiveCommodity: archiveCommodityUseCase, + createCommodity: createCommodityUseCase, + getAllCommodities: getAllCommoditiesUseCase, + getCommodityById: getCommodityByIdUseCase, + updateCommodity: updateCommodityUseCase, + }, transaction: { createTransaction: createTransactionUseCase, deleteTransaction: deleteTransactionUseCase, @@ -159,6 +201,7 @@ export const createContainer = (db: DataBase): AppContainer => { const controllers: AppContainer['controllers'] = { account: accountController, auth: authController, + commodity: commodityController, transaction: transactionController, user: userController, }; diff --git a/apps/backend/src/di/types.ts b/apps/backend/src/di/types.ts index d2b5613d..ccbe5f3a 100644 --- a/apps/backend/src/di/types.ts +++ b/apps/backend/src/di/types.ts @@ -1,16 +1,22 @@ -import { LoginUserUseCase, RegisterUserUseCase } from 'src/application'; -import { CreateAccountUseCase } from 'src/application/usecases/accounts/createAccount'; -import { DeleteAccountUseCase } from 'src/application/usecases/accounts/deleteAccount'; -import { GetAccountByIdUseCase } from 'src/application/usecases/accounts/getAccountById'; -import { GetAllAccountsUseCase } from 'src/application/usecases/accounts/getAllAccounts'; -import { UpdateAccountUseCase } from 'src/application/usecases/accounts/updateAccount'; import { + GetAllCommoditiesUseCase, + GetCommodityByIdUseCase, + LoginUserUseCase, + RegisterUserUseCase, + CreateAccountUseCase, + DeleteAccountUseCase, + GetAccountByIdUseCase, + GetAllAccountsUseCase, + UpdateAccountUseCase, DeleteTransactionUseCase, GetAllTransactionsUseCase, -} from 'src/application/usecases/transaction'; -import { CreateTransactionUseCase } from 'src/application/usecases/transaction/CreateTransaction'; -import { GetTransactionByIdUseCase } from 'src/application/usecases/transaction/GetTransactionById'; -import { UpdateTransactionUseCase } from 'src/application/usecases/transaction/UpdateTransaction'; + GetTransactionByIdUseCase, + UpdateTransactionUseCase, + CreateTransactionUseCase, + UpdateCommodityUseCase, + ArchiveCommodityUseCase, + CreateCommodityUseCase, +} from 'src/application'; import { DataBase } from 'src/db'; import { PasswordManager } from 'src/infrastructure/auth/PasswordManager'; import { @@ -24,6 +30,7 @@ import { AccountController, AuthController, TransactionController, + CommodityController, UserController, } from 'src/presentation/http'; @@ -60,11 +67,20 @@ type TransactionUseCases = { deleteTransaction: DeleteTransactionUseCase; }; +type CommodityUseCases = { + getCommodityById: GetCommodityByIdUseCase; + getAllCommodities: GetAllCommoditiesUseCase; + updateCommodity: UpdateCommodityUseCase; + archiveCommodity: ArchiveCommodityUseCase; + createCommodity: CreateCommodityUseCase; +}; + type Controllers = { account: AccountController; user: UserController; auth: AuthController; transaction: TransactionController; + commodity: CommodityController; }; export type AppContainer = { @@ -75,6 +91,7 @@ export type AppContainer = { account: AccountUseCases; auth: AuthUseCases; transaction: TransactionUseCases; + commodity: CommodityUseCases; }; controllers: Controllers; }; diff --git a/apps/backend/src/domain/commodities/types.ts b/apps/backend/src/domain/commodities/types.ts index de76f723..d63e6f27 100644 --- a/apps/backend/src/domain/commodities/types.ts +++ b/apps/backend/src/domain/commodities/types.ts @@ -4,7 +4,7 @@ import { IsoDatetimeString, UUID, CommodityPrecisionNumber, - NameString, + RequiredText, } from '@ledgerly/shared/types'; export type CommoditySnapshot = { @@ -15,19 +15,19 @@ export type CommoditySnapshot = { symbol: CommoditySymbolString | null; updatedAt: IsoDatetimeString; userId: UUID; - name: NameString; + name: RequiredText; precision: CommodityPrecisionNumber; }; export type CommodityUpdateProps = Partial<{ code: CommodityCodeString; symbol: CommoditySymbolString | null; - name: NameString; + name: RequiredText; }>; export type CreateCommodityProps = { code: CommodityCodeString; - name: NameString; + name: RequiredText; precision?: CommodityPrecisionNumber; symbol?: CommoditySymbolString | null; }; diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 22cad758..399775a6 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -355,9 +355,12 @@ describe('CommodityRepository', () => { updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }); - await expect( - commodityRepository.getById(user.id, commodityDbRow.id), - ).rejects.toThrowError(RepositoryNotFoundError); + const archivedCommodity = await testDB.getCommodityById( + commodityDbRow.id, + ); + + expect(archivedCommodity).toBeDefined(); + expect(archivedCommodity?.isTombstone).toBe(true); }); it('should throw an error when the commodity does not exist', async () => { diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index 01d98b6d..b59c40cb 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -22,11 +22,7 @@ export class CommodityRepository .select() .from(commoditiesTable) .where( - and( - eq(commoditiesTable.userId, userId), - eq(commoditiesTable.id, id), - eq(commoditiesTable.isTombstone, false), - ), + and(eq(commoditiesTable.userId, userId), eq(commoditiesTable.id, id)), ) .get(); diff --git a/apps/backend/src/presentation/http/accounts/accounts.routes.ts b/apps/backend/src/presentation/http/accounts/account.routes.ts similarity index 100% rename from apps/backend/src/presentation/http/accounts/accounts.routes.ts rename to apps/backend/src/presentation/http/accounts/account.routes.ts diff --git a/apps/backend/src/presentation/http/accounts/index.ts b/apps/backend/src/presentation/http/accounts/index.ts index 8ba7709e..c5db09e0 100644 --- a/apps/backend/src/presentation/http/accounts/index.ts +++ b/apps/backend/src/presentation/http/accounts/index.ts @@ -1,2 +1,2 @@ export { AccountController } from './account.controller'; -export { accountsRoutes } from './accounts.routes'; +export { accountsRoutes } from './account.routes'; diff --git a/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts b/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts new file mode 100644 index 00000000..fe83dd5a --- /dev/null +++ b/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts @@ -0,0 +1,218 @@ +import { + ArchiveCommodityUseCase, + CreateCommodityUseCase, + GetAllCommoditiesUseCase, + GetCommodityByIdUseCase, + UpdateCommodityUseCase, +} from 'src/application/usecases/commodities'; +import { CommodityCode } from 'src/domain/domain-core'; +import { Id } from 'src/domain/domain-core/value-objects/Id'; +import { User } from 'src/domain/users/user.entity'; +import { createUser } from 'src/testing'; +import { describe, vi, beforeEach, it, expect } from 'vitest'; +import { ZodError } from 'zod'; + +import { CommodityController } from './commodity.controller'; + +describe('CommodityController', () => { + let user: User; + + const commodityId = Id.restore( + 'b2035d76-f6b1-4546-8637-6f034f4ade50', + ).valueOf(); + + const mockGetCommodityByIdUseCase = { + execute: vi.fn(), + }; + + const mockGetAllCommoditiesUseCase = { + execute: vi.fn(), + }; + + const mockCreateCommodityUseCase = { + execute: vi.fn(), + }; + + const mockUpdateCommodityUseCase = { + execute: vi.fn(), + }; + + const mockArchiveCommodityUseCase = { + execute: vi.fn(), + }; + + const commodityController = new CommodityController( + mockGetCommodityByIdUseCase as unknown as GetCommodityByIdUseCase, + mockGetAllCommoditiesUseCase as unknown as GetAllCommoditiesUseCase, + mockCreateCommodityUseCase as unknown as CreateCommodityUseCase, + mockUpdateCommodityUseCase as unknown as UpdateCommodityUseCase, + mockArchiveCommodityUseCase as unknown as ArchiveCommodityUseCase, + ); + + beforeEach(async () => { + user = await createUser(); + + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should call getAllCommoditiesUseCase.execute with correct user', async () => { + await commodityController.getAll(user); + + expect(mockGetAllCommoditiesUseCase.execute).toHaveBeenCalledWith(user); + expect(mockGetAllCommoditiesUseCase.execute).toHaveBeenCalledTimes(1); + }); + }); + + describe('getById', () => { + it('should call getCommodityByIdUseCase.execute with correct user and id', async () => { + const requestParams = { id: commodityId }; + + await commodityController.getById(user, requestParams); + + expect(mockGetCommodityByIdUseCase.execute).toHaveBeenCalledWith( + user, + requestParams.id, + ); + expect(mockGetCommodityByIdUseCase.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw ZodError if requestParams is invalid', async () => { + const invalidRequestParams = { invalidId: 'commodity-id' }; + + await expect( + commodityController.getById(user, invalidRequestParams), + ).rejects.toThrow(ZodError); + }); + }); + + describe('create', () => { + it('should call createCommodityUseCase.execute with correct user and data', async () => { + const requestBody = { + code: CommodityCode.create('CommodityCode').valueOf(), + name: 'commodityName', + precision: 2, + symbol: null, + }; + + await commodityController.create(user, requestBody); + + expect(mockCreateCommodityUseCase.execute).toHaveBeenCalledWith( + user, + requestBody, + ); + + expect(mockCreateCommodityUseCase.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw ZodError if requestBody is invalid', async () => { + const invalidRequestBody = { invalidField: 'invalidValue' }; + + await expect( + commodityController.create(user, invalidRequestBody), + ).rejects.toThrow(ZodError); + }); + + it('should allow to create commodity without precision', async () => { + const requestBody = { + code: CommodityCode.create('UpdatedCode').valueOf(), + name: 'Updated Name', + }; + + await commodityController.create(user, requestBody); + + expect(mockCreateCommodityUseCase.execute).toHaveBeenCalledWith( + user, + requestBody, + ); + }); + }); + + describe('update', () => { + const requestBody = { + code: CommodityCode.create('UpdatedCode').valueOf(), + name: 'Updated Name', + symbol: '$', + }; + + it('should call updateCommodityUseCase.execute with correct user, id and data', async () => { + const requestParams = { id: commodityId }; + + await commodityController.update(user, requestParams, requestBody); + + expect(mockUpdateCommodityUseCase.execute).toHaveBeenCalledWith( + user, + requestParams.id, + requestBody, + ); + + expect(mockUpdateCommodityUseCase.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw ZodError if requestBody is invalid', async () => { + const requestParams = { id: commodityId }; + const invalidRequestBody = { code: 'invalid-value' }; + + await expect( + commodityController.update(user, requestParams, invalidRequestBody), + ).rejects.toThrow(ZodError); + }); + + it('should ignore precision in update', async () => { + const requestParams = { id: commodityId }; + const requestBody = { + code: 'USD', + name: 'US Dollar', + precision: 3, + symbol: '$', + }; + + const expectedData = { + code: requestBody.code, + name: requestBody.name, + symbol: requestBody.symbol, + }; + + await commodityController.update(user, requestParams, requestBody); + + expect(mockUpdateCommodityUseCase.execute).toHaveBeenCalledWith( + user, + requestParams.id, + expectedData, + ); + + expect(mockUpdateCommodityUseCase.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw ZodError if requestParams is invalid', async () => { + const invalidRequestParams = { invalidId: 'commodity-id' }; + + await expect( + commodityController.update(user, invalidRequestParams, requestBody), + ).rejects.toThrow(ZodError); + }); + }); + + describe('archiveCommodity', () => { + it('should call archiveCommodityUseCase.execute with correct user and id', async () => { + const requestParams = { id: commodityId }; + + await commodityController.archiveCommodity(user, requestParams); + + expect(mockArchiveCommodityUseCase.execute).toHaveBeenCalledWith( + user, + requestParams.id, + ); + + expect(mockArchiveCommodityUseCase.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw ZodError if requestParams is invalid', async () => { + const invalidRequestParams = { invalidId: 'commodity-id' }; + + await expect( + commodityController.archiveCommodity(user, invalidRequestParams), + ).rejects.toThrow(ZodError); + }); + }); +}); diff --git a/apps/backend/src/presentation/http/commodities/commodity.controller.ts b/apps/backend/src/presentation/http/commodities/commodity.controller.ts new file mode 100644 index 00000000..74b3cb21 --- /dev/null +++ b/apps/backend/src/presentation/http/commodities/commodity.controller.ts @@ -0,0 +1,66 @@ +import { CommodityResponseDTO } from '@ledgerly/shared/types'; +import { + commodityCreateSchema, + commodityUpdateSchema, + uniqueIdSchema, +} from '@ledgerly/shared/validation'; +import { + GetCommodityByIdUseCase, + ArchiveCommodityUseCase, + CreateCommodityUseCase, + GetAllCommoditiesUseCase, + UpdateCommodityUseCase, +} from 'src/application/usecases/commodities'; +import { User } from 'src/domain/users/user.entity'; + +export class CommodityController { + constructor( + private readonly getCommodityByIdUseCase: GetCommodityByIdUseCase, + private readonly getAllCommoditiesUseCase: GetAllCommoditiesUseCase, + private readonly createCommodityUseCase: CreateCommodityUseCase, + private readonly updateCommodityUseCase: UpdateCommodityUseCase, + private readonly archiveCommodityUseCase: ArchiveCommodityUseCase, + ) {} + + async getAll(user: User): Promise { + return this.getAllCommoditiesUseCase.execute(user); + } + + async getById( + user: User, + requestParams: unknown, + ): Promise { + const { id } = uniqueIdSchema.parse(requestParams); + + return this.getCommodityByIdUseCase.execute(user, id); + } + + async create( + user: User, + requestBody: unknown, + ): Promise { + const commodityCreateDto = commodityCreateSchema.parse(requestBody); + + return this.createCommodityUseCase.execute(user, commodityCreateDto); + } + + async update( + user: User, + requestParams: unknown, + requestBody: unknown, + ): Promise { + const { id } = uniqueIdSchema.parse(requestParams); + const commodityUpdateDto = commodityUpdateSchema.parse(requestBody); + + return this.updateCommodityUseCase.execute(user, id, commodityUpdateDto); + } + + async archiveCommodity( + user: User, + requestParams: unknown, + ): Promise { + const { id } = uniqueIdSchema.parse(requestParams); + + return this.archiveCommodityUseCase.execute(user, id); + } +} diff --git a/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts b/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts new file mode 100644 index 00000000..d4bf144f --- /dev/null +++ b/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts @@ -0,0 +1,118 @@ +import { ROUTES } from '@ledgerly/shared/routes'; +import { UUID } from '@ledgerly/shared/types'; +import { TestDB } from 'src/db/test-db'; +import { Id } from 'src/domain/domain-core'; +import { createServer } from 'src/presentation/http'; +import { beforeEach, describe, it } from 'vitest'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const url = `/api${ROUTES.commodities}`; + +const testUser = { + email: 'test@example.com', + name: 'Test User', + password: 'Password123!', +}; + +describe('Commodities Integration Tests', () => { + let testDB: TestDB; + let server: ReturnType; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let authToken: string; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let userId: UUID; + + beforeEach(async () => { + testDB = new TestDB(); + server = createServer(testDB.db); + await testDB.setupTestDb(); + + await server.ready(); + + const user = await testDB.createUser(testUser); + + const token = server.jwt.sign({ + email: user.email, + userId: user.id, + }); + + authToken = token; + + const decoded = server.jwt.decode(token) as unknown as { userId: UUID }; + userId = Id.restore(decoded.userId).valueOf(); + }); + + describe('GET /api/commodities', () => { + it.todo('should return all commodities for the authenticated user'); + + it.todo('should not return commodities belonging to another user'); + + it.todo('should include archived commodities marked as archived'); + + it.todo('should return 401 without authorization'); + }); + + describe('GET /api/commodities/:id', () => { + it.todo('should return a commodity by id for the authenticated user'); + + it.todo('should return an archived commodity by id marked as archived'); + + it.todo('should return 404 when the commodity does not exist'); + + it.todo('should return 404 when the commodity belongs to another user'); + + it.todo('should return 400 when the commodity id is invalid'); + + it.todo('should return 401 without authorization'); + }); + + describe('POST /api/commodities', () => { + it.todo('should create a commodity for the authenticated user'); + + it.todo('should create a commodity with default precision and null symbol'); + + it.todo('should reject duplicate code for the same user'); + + it.todo('should allow the same code for different users'); + + it.todo('should return 400 for invalid payload'); + + it.todo('should return 401 without authorization'); + }); + + describe('PUT /api/commodities/:id', () => { + it.todo('should update editable commodity metadata'); + + it.todo('should reject duplicate code for the same user'); + + it.todo('should allow updating to a code used by another user'); + + it.todo('should return 404 when updating another user commodity'); + + it.todo('should return 404 when updating an archived commodity'); + + it.todo('should return 400 for invalid params'); + + it.todo('should return 400 for invalid payload'); + + it.todo('should return 401 without authorization'); + }); + + describe('DELETE /api/commodities/:id', () => { + it.todo('should archive a commodity by id'); + + it.todo('should leave the archived commodity readable by id'); + + it.todo( + 'should return the archived commodity in the list marked as archived', + ); + + it.todo('should return 404 when archiving another user commodity'); + + it.todo('should return 404 when archiving an already archived commodity'); + + it.todo('should return 400 when the commodity id is invalid'); + + it.todo('should return 401 without authorization'); + }); +}); diff --git a/apps/backend/src/presentation/http/commodities/commodity.routes.ts b/apps/backend/src/presentation/http/commodities/commodity.routes.ts new file mode 100644 index 00000000..a4f08148 --- /dev/null +++ b/apps/backend/src/presentation/http/commodities/commodity.routes.ts @@ -0,0 +1,44 @@ +import type { FastifyInstance } from 'fastify'; + +export const commodityRoutes = (app: FastifyInstance) => { + const commodityController = app.container.controllers.commodity; + + app.get('/', async (request) => { + const user = request.user; + + return await commodityController.getAll(user); + }); + + app.get('/:id', async (request) => { + const user = request.user; + + return commodityController.getById(user, request.params); + }); + + app.post('/', async (request, reply) => { + const user = request.user; + + const commodity = await commodityController.create(user, request.body); + reply.status(201).send(commodity); + }); + + app.delete('/:id', async (request, reply) => { + const user = request.user; + + await commodityController.archiveCommodity(user, request.params); + + reply.status(204).send(); + }); + + // or patch + app.put('/:id', async (request, reply) => { + const user = request.user; + + const updatedCommodity = await commodityController.update( + user, + request.params, + request.body, + ); + reply.status(200).send(updatedCommodity); + }); +}; diff --git a/apps/backend/src/presentation/http/commodities/index.ts b/apps/backend/src/presentation/http/commodities/index.ts new file mode 100644 index 00000000..e8e3f582 --- /dev/null +++ b/apps/backend/src/presentation/http/commodities/index.ts @@ -0,0 +1,2 @@ +export { CommodityController } from './commodity.controller'; +export { commodityRoutes } from './commodity.routes'; diff --git a/apps/backend/src/presentation/http/index.ts b/apps/backend/src/presentation/http/index.ts index 000a78ee..24fe42a9 100644 --- a/apps/backend/src/presentation/http/index.ts +++ b/apps/backend/src/presentation/http/index.ts @@ -6,3 +6,4 @@ export { registerRoutes } from './register-routes'; export { createServer } from './server'; export { TransactionController, transactionsRoutes } from './transactions'; export { registerUserRoutes, UserController } from './users'; +export { CommodityController } from './commodities'; diff --git a/packages/shared/src/types/account.ts b/packages/shared/src/types/account.ts index 80993e68..28dbaea9 100644 --- a/packages/shared/src/types/account.ts +++ b/packages/shared/src/types/account.ts @@ -1,4 +1,4 @@ -import { IsoDatetimeString, AmountString, UUID, NameString } from "./types"; +import { IsoDatetimeString, AmountString, UUID, RequiredText } from "./types"; export type AccountTypeValue = | "asset" @@ -29,7 +29,7 @@ export type AccountResponseDTO = { initialBalance: AmountString; isSystem: boolean; isTombstone: boolean; - name: NameString; + name: RequiredText; type: AccountTypeValue; updatedAt: IsoDatetimeString; userId: UUID; diff --git a/packages/shared/src/types/commodity.ts b/packages/shared/src/types/commodity.ts index 83b3575a..66b745ca 100644 --- a/packages/shared/src/types/commodity.ts +++ b/packages/shared/src/types/commodity.ts @@ -3,13 +3,13 @@ import { CommodityPrecisionNumber, CommoditySymbolString, IsoDatetimeString, - NameString, + RequiredText, UUID, } from "./types"; export type CommodityCreateDTO = { code: CommodityCodeString; - name: NameString; + name: RequiredText; precision?: CommodityPrecisionNumber; symbol?: CommoditySymbolString | null; }; @@ -19,9 +19,13 @@ export type CommodityResponseDTO = { createdAt: IsoDatetimeString; id: UUID; isTombstone: boolean; - name: NameString; + name: RequiredText; precision: CommodityPrecisionNumber; symbol: CommoditySymbolString | null; updatedAt: IsoDatetimeString; userId: UUID; }; + +export type CommodityUpdateDTO = Partial< + Pick +>; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 387d412a..9669b6d3 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -20,4 +20,4 @@ export type AmountString = z.infer; export type CommodityCodeString = z.infer; export type CommodityPrecisionNumber = z.infer; export type CommoditySymbolString = z.infer; -export type NameString = z.infer; +export type RequiredText = z.infer; diff --git a/packages/shared/src/validation/accounts.ts b/packages/shared/src/validation/accounts.ts index 6d6c98b6..853da1a5 100644 --- a/packages/shared/src/validation/accounts.ts +++ b/packages/shared/src/validation/accounts.ts @@ -3,8 +3,8 @@ import { z } from "zod"; import { ACCOUNT_TYPE_VALUES } from "../constants"; import { - notNullText, requiredText, + textWithDefault, uuid, amountString, } from "./baseValidations"; @@ -13,9 +13,9 @@ const accountType = z.enum(ACCOUNT_TYPE_VALUES); export const accountCreateSchema = z.object({ commodityId: uuid, - description: requiredText, + description: textWithDefault, initialBalance: amountString, - name: notNullText, + name: requiredText, type: accountType, }); diff --git a/packages/shared/src/validation/baseValidations.ts b/packages/shared/src/validation/baseValidations.ts index 7262cd59..cbf1bab9 100644 --- a/packages/shared/src/validation/baseValidations.ts +++ b/packages/shared/src/validation/baseValidations.ts @@ -10,23 +10,17 @@ import { DEFAULT_TRANSACTION_SORT_ORDER, } from "../constants/transactions"; -export const requiredText = z.string().default(""); - -export const defaultText = z.string().default("").optional(); -export const defaultNumber = z.number().default(0).optional(); -export const notNullText = z.string().min(1, "This field cannot be empty"); +export const textWithDefault = z.string().default(""); +export const requiredText = z.string(); export const updatedAt = z.string(); export const createdAt = z.string(); export const uuid = z.string().uuid().brand<"UUID">(); -export const name = z.string().min(1); export const dateText = z.string().refine((d) => !isNaN(Date.parse(d)), { message: "Invalid date format", }); - export const uniqueIdSchema = z.object({ id: uuid, }); - export const isoDatetime = z.string().datetime().brand<"IsoDatetimeString">(); export const sha256String = z.string().regex(/^[a-f0-9]{64}$/, { @@ -78,7 +72,6 @@ export const getTransactionsQuerySchema = z const COMMODITY_CODE_MIN_LENGTH = 3; const COMMODITY_CODE_MAX_LENGTH = 16; - export const commodityCode = z .string() .trim() @@ -96,7 +89,5 @@ export const commodityCode = z "Commodity code must start with a letter and contain only letters and digits", ) .brand<"CommodityCode">(); - export const commoditySymbol = z.string().trim().max(12).nullable(); - export const commodityPrecision = z.number().int().min(0).max(18); diff --git a/packages/shared/src/validation/commodities.ts b/packages/shared/src/validation/commodities.ts new file mode 100644 index 00000000..08e26543 --- /dev/null +++ b/packages/shared/src/validation/commodities.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +import { + requiredText, + commodityPrecision, + commoditySymbol, + commodityCode, +} from "./baseValidations"; + +export const commodityCreateSchema = z.object({ + code: commodityCode, + name: requiredText, + precision: commodityPrecision.optional(), + symbol: commoditySymbol.nullable().optional(), +}); + +export const commodityUpdateSchema = commodityCreateSchema + .pick({ + code: true, + name: true, + symbol: true, + }) + .partial(); diff --git a/packages/shared/src/validation/index.ts b/packages/shared/src/validation/index.ts index d9a19085..1147d7eb 100644 --- a/packages/shared/src/validation/index.ts +++ b/packages/shared/src/validation/index.ts @@ -3,3 +3,4 @@ export * from "./transactions"; export * from "./users"; export * from "./auth"; export * from "./baseValidations"; +export * from "./commodities"; diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index 9ede40d4..364a5e35 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -5,7 +5,12 @@ import { MIN_TRANSACTION_OPERATIONS, } from "../constants/transactions"; -import { uuid, requiredText, isoDate, amountString } from "./baseValidations"; +import { + uuid, + textWithDefault, + isoDate, + amountString, +} from "./baseValidations"; // amount — posting in the account Commodity // value — posting in the transaction valuation Commodity (GnuCash convention) @@ -14,7 +19,7 @@ import { uuid, requiredText, isoDate, amountString } from "./baseValidations"; export const operationCreateSchema = z.object({ accountId: uuid, amount: amountString, - description: requiredText, + description: textWithDefault, value: amountString, }); @@ -22,14 +27,14 @@ export const operationCreateSchema = z.object({ export const operationUpdateSchema = z.object({ accountId: uuid, amount: amountString, - description: requiredText, + description: textWithDefault, id: uuid, value: amountString, }); export const transactionCreateSchema = z.object({ commodityId: uuid, - description: requiredText, + description: textWithDefault, operations: z .array(operationCreateSchema) .min(MIN_TRANSACTION_OPERATIONS) @@ -39,7 +44,7 @@ export const transactionCreateSchema = z.object({ }); export const transactionUpdateSchema = z.object({ - description: requiredText, + description: textWithDefault, operations: z.object({ create: z.array(operationCreateSchema).max(MAX_TRANSACTION_OPERATIONS), delete: z.array(uuid).max(MAX_TRANSACTION_OPERATIONS), diff --git a/packages/shared/src/validation/users.ts b/packages/shared/src/validation/users.ts index 92b97b2d..0a2ac80d 100644 --- a/packages/shared/src/validation/users.ts +++ b/packages/shared/src/validation/users.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { notNullText } from "./baseValidations"; +import { requiredText } from "./baseValidations"; const password = z .string() @@ -12,7 +12,7 @@ const password = z export const usersCreateSchema = z.object({ email: z.string().email().toLowerCase().trim().min(1).max(255), - name: notNullText, + name: requiredText, password, }); @@ -25,7 +25,7 @@ export const usersResponseSchema = z export const usersUpdateSchema = z .object({ email: z.string().email().toLowerCase().trim().min(1).max(255).optional(), - name: notNullText.optional(), + name: requiredText.optional(), }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -33,6 +33,6 @@ export const usersUpdateSchema = z }); export const passwordChangeSchema = z.object({ - currentPassword: notNullText, + currentPassword: requiredText, newPassword: password, }); From 2210b2f6fc863f361e3fa5141d4201917bfbc127 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Sun, 26 Jul 2026 10:41:50 +0300 Subject: [PATCH 23/30] LED-110 feat(commodities): add query status handling in getAll operations --- .../CommodityRepository.interface.ts | 4 +- .../__tests__/getAllCommodities.test.ts | 6 +- .../usecases/commodities/getAllCommodities.ts | 8 +- .../commodities/commodity.repository.test.ts | 94 ++++++++++++++----- .../db/commodities/commodity.repository.ts | 19 ++-- .../commodities/commodity.controller.test.ts | 56 +++++++++-- .../http/commodities/commodity.controller.ts | 10 +- .../http/commodities/commodity.routes.ts | 2 +- apps/backend/src/presentation/http/index.ts | 2 +- .../src/presentation/http/register-routes.ts | 5 + packages/shared/src/types/types.ts | 4 + .../shared/src/validation/baseValidations.ts | 10 ++ 12 files changed, 171 insertions(+), 49 deletions(-) diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts index 18078ba4..d9c03b75 100644 --- a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -1,4 +1,4 @@ -import { UUID } from '@ledgerly/shared/types'; +import { QueryStatus, UUID } from '@ledgerly/shared/types'; import { CommoditySnapshot } from 'src/domain/commodities'; export type CommodityRepositoryUpdateInput = Pick< @@ -14,7 +14,7 @@ export type CommodityRepositorySoftDeleteInput = Pick< export type CommodityRepositoryInterface = { getById(userId: UUID, commodityId: UUID): Promise; - getAll(userId: UUID): Promise; + getAll(userId: UUID, status: QueryStatus): Promise; create(commodity: CommoditySnapshot): Promise; update( userId: UUID, diff --git a/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts index 645677af..ee5c908b 100644 --- a/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts +++ b/apps/backend/src/application/usecases/commodities/__tests__/getAllCommodities.test.ts @@ -52,15 +52,19 @@ describe('GetAllCommoditiesUseCase', () => { }, ]; + const queryStatus = 'active'; + commodityRepository.getAll.mockResolvedValue(mockCommodities); // Act - const result = await getAllCommoditiesUseCase.execute(user); + const result = await getAllCommoditiesUseCase.execute(user, queryStatus); // Assert expect(commodityRepository.getAll).toHaveBeenCalledWith( user.getId().valueOf(), + queryStatus, ); + expect(result).toEqual( mockCommodities.map((commodity) => ({ code: commodity.code, diff --git a/apps/backend/src/application/usecases/commodities/getAllCommodities.ts b/apps/backend/src/application/usecases/commodities/getAllCommodities.ts index c246a780..7f8e6da2 100644 --- a/apps/backend/src/application/usecases/commodities/getAllCommodities.ts +++ b/apps/backend/src/application/usecases/commodities/getAllCommodities.ts @@ -1,4 +1,4 @@ -import { CommodityResponseDTO } from '@ledgerly/shared/types'; +import { CommodityResponseDTO, QueryStatus } from '@ledgerly/shared/types'; import { CommodityMapper } from 'src/application'; import { CommodityRepositoryInterface } from 'src/application/interfaces'; import { User } from 'src/domain/users/user.entity'; @@ -8,9 +8,13 @@ export class GetAllCommoditiesUseCase { protected readonly commodityRepository: CommodityRepositoryInterface, ) {} - async execute(user: User): Promise { + async execute( + user: User, + status: QueryStatus, + ): Promise { const commoditiesSnapshots = await this.commodityRepository.getAll( user.getId().valueOf(), + status, ); return commoditiesSnapshots.map((commoditySnapshot) => diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 399775a6..10054119 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -1,4 +1,4 @@ -import { apiErrorCodes } from '@ledgerly/shared/types'; +import { apiErrorCodes, type QueryStatus } from '@ledgerly/shared/types'; import { type CommodityRepositoryUpdateInput } from 'src/application'; import type { CommodityDbRow } from 'src/db/schemas/commodities'; import { UserDbRow } from 'src/db/schemas/users'; @@ -84,28 +84,60 @@ describe('CommodityRepository', () => { }); describe('getAll', () => { - it('should return all commodities for the user', async () => { - const userCommodities = await Promise.all([ - testDB.createCommodity(user.id), - testDB.createCommodity(user.id), - ]); - - const commodities = await commodityRepository.getAll(user.id); - - expect(commodities).toEqual(expect.arrayContaining(userCommodities)); - - expect(commodities).toHaveLength(userCommodities.length); - }); + const queryStatusCases: { + expectedTombstoneValues: boolean[]; + status: QueryStatus; + }[] = [ + { expectedTombstoneValues: [false, false], status: 'active' }, + { expectedTombstoneValues: [true], status: 'archived' }, + { expectedTombstoneValues: [false, false, true], status: 'all' }, + ]; + + it.each(queryStatusCases)( + 'should return $status commodities for the user', + async ({ expectedTombstoneValues, status }) => { + const activeCommodities = await Promise.all([ + testDB.createCommodity(user.id), + testDB.createCommodity(user.id), + ]); + + const archivedCommodity = await testDB.createCommodity(user.id, { + isTombstone: true, + }); + + const commodities = await commodityRepository.getAll(user.id, status); + + const expectedCommodities = + status === 'active' + ? activeCommodities + : status === 'archived' + ? [archivedCommodity] + : [...activeCommodities, archivedCommodity]; + + expect(commodities).toEqual( + expect.arrayContaining(expectedCommodities), + ); + + expect(commodities).toHaveLength(expectedCommodities.length); + + expect( + commodities.map(({ isTombstone }) => isTombstone).sort(), + ).toEqual(expectedTombstoneValues.sort()); + }, + ); it('should return an empty array when the user has no commodities', async () => { const anotherUser = await testDB.createUser(); - const commodities = await commodityRepository.getAll(anotherUser.id); + const commodities = await commodityRepository.getAll( + anotherUser.id, + 'active', + ); expect(commodities).toEqual([]); }); - it('should not return tombstoned commodities', async () => { + it('should not return tombstoned commodities for active status', async () => { const commodity1 = await testDB.createCommodity(user.id); const commodity2 = await testDB.createCommodity(user.id); @@ -113,22 +145,32 @@ describe('CommodityRepository', () => { updatedAt: Timestamp.create().valueOf(), }); - const commodities = await commodityRepository.getAll(user.id); + const commodities = await commodityRepository.getAll(user.id, 'active'); expect(commodities).toEqual(expect.arrayContaining([commodity2])); expect(commodities).not.toEqual(expect.arrayContaining([commodity1])); }); - it('should not return commodities belonging to other users', async () => { - const commodity1 = await testDB.createCommodity(user.id); - const anotherUser = await testDB.createUser(); - const commodity2 = await testDB.createCommodity(anotherUser.id); - - const commodities = await commodityRepository.getAll(user.id); - - expect(commodities).toEqual(expect.arrayContaining([commodity1])); - expect(commodities).not.toEqual(expect.arrayContaining([commodity2])); - }); + it.each(['active', 'archived', 'all'])( + 'should not return commodities belonging to other users for %s status', + async (status) => { + const commodity1 = await testDB.createCommodity(user.id); + const anotherUser = await testDB.createUser(); + const commodity2 = await testDB.createCommodity(anotherUser.id, { + isTombstone: status === 'archived', + }); + + if (status === 'archived') { + await commodityRepository.delete(user.id, commodity1.id, { + updatedAt: Timestamp.create().valueOf(), + }); + } + + const commodities = await commodityRepository.getAll(user.id, status); + + expect(commodities).not.toEqual(expect.arrayContaining([commodity2])); + }, + ); }); describe('create', () => { diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index b59c40cb..991bae72 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -1,4 +1,4 @@ -import { UUID } from '@ledgerly/shared/types'; +import { QueryStatus, UUID } from '@ledgerly/shared/types'; import { and, eq } from 'drizzle-orm'; import { type CommodityRepositoryInterface, @@ -36,17 +36,20 @@ export class CommodityRepository }, 'Failed to fetch commodity'); } - getAll(userId: UUID): Promise { + getAll(userId: UUID, status: QueryStatus): Promise { return this.executeDatabaseOperation(async () => { + const whereClause = + status === 'all' + ? eq(commoditiesTable.userId, userId) + : and( + eq(commoditiesTable.userId, userId), + eq(commoditiesTable.isTombstone, status === 'archived'), + ); + const commodities = await this.db .select() .from(commoditiesTable) - .where( - and( - eq(commoditiesTable.userId, userId), - eq(commoditiesTable.isTombstone, false), - ), - ) + .where(whereClause) .all(); return commodities.map((commodity) => diff --git a/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts b/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts index fe83dd5a..7d0b55a3 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts @@ -1,3 +1,4 @@ +import { type QueryStatus } from '@ledgerly/shared/types'; import { ArchiveCommodityUseCase, CreateCommodityUseCase, @@ -56,12 +57,55 @@ describe('CommodityController', () => { }); describe('getAll', () => { - it('should call getAllCommoditiesUseCase.execute with correct user', async () => { - await commodityController.getAll(user); - - expect(mockGetAllCommoditiesUseCase.execute).toHaveBeenCalledWith(user); - expect(mockGetAllCommoditiesUseCase.execute).toHaveBeenCalledTimes(1); - }); + const validQueryParams: { + expectedStatus: QueryStatus; + queryParams: unknown; + }[] = [ + { expectedStatus: 'active', queryParams: { status: 'active' } }, + { expectedStatus: 'archived', queryParams: { status: 'archived' } }, + { expectedStatus: 'all', queryParams: { status: 'all' } }, + { expectedStatus: 'active', queryParams: {} }, + ]; + + it.each(validQueryParams)( + 'should call getAllCommoditiesUseCase.execute with $expectedStatus status', + async ({ expectedStatus, queryParams }) => { + await commodityController.getAll(user, queryParams); + + expect(mockGetAllCommoditiesUseCase.execute).toHaveBeenCalledWith( + user, + expectedStatus, + ); + expect(mockGetAllCommoditiesUseCase.execute).toHaveBeenCalledTimes(1); + }, + ); + + const invalidQueryParams = [ + { invalidParam: 'invalidValue' }, + { status: 'deleted' }, + { status: '' }, + { status: null }, + { status: 1 }, + { status: true }, + 'active', + 'deleted', + '', + undefined, + null, + 1, + true, + ]; + + it.each(invalidQueryParams)( + 'should throw ZodError if queryParams is invalid: %s', + async (queryParams) => { + await expect( + commodityController.getAll(user, queryParams), + ).rejects.toThrow(ZodError); + + expect(mockGetAllCommoditiesUseCase.execute).not.toHaveBeenCalled(); + }, + ); }); describe('getById', () => { diff --git a/apps/backend/src/presentation/http/commodities/commodity.controller.ts b/apps/backend/src/presentation/http/commodities/commodity.controller.ts index 74b3cb21..cd1f60b6 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.controller.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.controller.ts @@ -3,6 +3,7 @@ import { commodityCreateSchema, commodityUpdateSchema, uniqueIdSchema, + queryStatusSchema, } from '@ledgerly/shared/validation'; import { GetCommodityByIdUseCase, @@ -22,8 +23,13 @@ export class CommodityController { private readonly archiveCommodityUseCase: ArchiveCommodityUseCase, ) {} - async getAll(user: User): Promise { - return this.getAllCommoditiesUseCase.execute(user); + async getAll( + user: User, + queryParams: unknown, + ): Promise { + const { status } = queryStatusSchema.parse(queryParams); + + return this.getAllCommoditiesUseCase.execute(user, status); } async getById( diff --git a/apps/backend/src/presentation/http/commodities/commodity.routes.ts b/apps/backend/src/presentation/http/commodities/commodity.routes.ts index a4f08148..dadafcb1 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.routes.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.routes.ts @@ -6,7 +6,7 @@ export const commodityRoutes = (app: FastifyInstance) => { app.get('/', async (request) => { const user = request.user; - return await commodityController.getAll(user); + return await commodityController.getAll(user, request.query); }); app.get('/:id', async (request) => { diff --git a/apps/backend/src/presentation/http/index.ts b/apps/backend/src/presentation/http/index.ts index 24fe42a9..a7d91b13 100644 --- a/apps/backend/src/presentation/http/index.ts +++ b/apps/backend/src/presentation/http/index.ts @@ -6,4 +6,4 @@ export { registerRoutes } from './register-routes'; export { createServer } from './server'; export { TransactionController, transactionsRoutes } from './transactions'; export { registerUserRoutes, UserController } from './users'; -export { CommodityController } from './commodities'; +export { CommodityController, commodityRoutes } from './commodities'; diff --git a/apps/backend/src/presentation/http/register-routes.ts b/apps/backend/src/presentation/http/register-routes.ts index ed0efb05..f275141f 100644 --- a/apps/backend/src/presentation/http/register-routes.ts +++ b/apps/backend/src/presentation/http/register-routes.ts @@ -3,6 +3,7 @@ import type { FastifyInstance } from 'fastify'; import { accountsRoutes } from './accounts'; import { authRoutes } from './auth'; +import { commodityRoutes } from './commodities'; import { authMiddleware } from './middleware'; import { transactionsRoutes } from './transactions'; import { registerUserRoutes } from './users'; @@ -30,5 +31,9 @@ export const registerRoutes = (fastify: FastifyInstance) => { protectedApp.register(registerUserRoutes, { prefix: ROUTES.user, }); + + protectedApp.register(commodityRoutes, { + prefix: ROUTES.commodities, + }); }); }; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 9669b6d3..ba2f7c0d 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -9,6 +9,8 @@ import { commodityCode, commodityPrecision, commoditySymbol, + queryStatus, + queryStatusSchema, requiredText, } from "../validation/baseValidations"; @@ -21,3 +23,5 @@ export type CommodityCodeString = z.infer; export type CommodityPrecisionNumber = z.infer; export type CommoditySymbolString = z.infer; export type RequiredText = z.infer; +export type QueryStatus = z.infer; +export type QueryStatusQuery = z.infer; diff --git a/packages/shared/src/validation/baseValidations.ts b/packages/shared/src/validation/baseValidations.ts index cbf1bab9..d1937b3b 100644 --- a/packages/shared/src/validation/baseValidations.ts +++ b/packages/shared/src/validation/baseValidations.ts @@ -91,3 +91,13 @@ export const commodityCode = z .brand<"CommodityCode">(); export const commoditySymbol = z.string().trim().max(12).nullable(); export const commodityPrecision = z.number().int().min(0).max(18); + +export const queryStatus = z + .enum(["active", "archived", "all"]) + .default("active"); + +export const queryStatusSchema = z + .object({ + status: queryStatus, + }) + .strict(); From 210a57f3be08afa7b7c50e430b1be1a0e5f6bf71 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Sun, 26 Jul 2026 21:20:02 +0300 Subject: [PATCH 24/30] LED-110 feat(commodities): implement archive and update use cases with error handling --- .../usecases/commodities/archiveCommodity.ts | 8 + .../usecases/commodities/updateCommodity.ts | 8 + apps/backend/src/db/schemas/index.ts | 2 +- apps/backend/src/db/test-db.ts | 50 +- .../src/db/test-utils/entityComparer.test.ts | 87 ++ .../src/db/test-utils/entityComparer.ts | 86 ++ apps/backend/src/db/test-utils/index.ts | 3 +- .../src/db/test-utils/parseResponse.ts | 3 + .../domain/commodities/commodity.entity.ts | 11 +- .../commodities/commodity.integration.test.ts | 763 +++++++++++++++++- .../src/presentation/http/error-handler.ts | 14 +- .../transaction.integration.test.ts | 6 +- 12 files changed, 989 insertions(+), 52 deletions(-) create mode 100644 apps/backend/src/db/test-utils/entityComparer.test.ts create mode 100644 apps/backend/src/db/test-utils/parseResponse.ts diff --git a/apps/backend/src/application/usecases/commodities/archiveCommodity.ts b/apps/backend/src/application/usecases/commodities/archiveCommodity.ts index b960c116..13c5a133 100644 --- a/apps/backend/src/application/usecases/commodities/archiveCommodity.ts +++ b/apps/backend/src/application/usecases/commodities/archiveCommodity.ts @@ -1,5 +1,6 @@ import { CommodityResponseDTO, UUID } from '@ledgerly/shared/types'; import { CommodityMapper } from 'src/application'; +import { EntityNotFoundError } from 'src/application/application.errors'; import { CommodityRepositoryInterface } from 'src/application/interfaces'; import { EnsureOwnedSnapshotFn } from 'src/application/shared/ensureOwnedSnapshot'; import { Commodity, CommoditySnapshot } from 'src/domain/commodities'; @@ -20,6 +21,13 @@ export class ArchiveCommodityUseCase { user, }); + if (commodityData.isTombstone) { + throw new EntityNotFoundError({ + entityId: commodityId, + entityType: Commodity.entityType, + }); + } + const commodity = Commodity.restore(commodityData); commodity.markAsDeleted(); diff --git a/apps/backend/src/application/usecases/commodities/updateCommodity.ts b/apps/backend/src/application/usecases/commodities/updateCommodity.ts index 82a46b0e..0ef19de6 100644 --- a/apps/backend/src/application/usecases/commodities/updateCommodity.ts +++ b/apps/backend/src/application/usecases/commodities/updateCommodity.ts @@ -4,6 +4,7 @@ import { CommodityUpdateDTO, } from '@ledgerly/shared/types'; import { CommodityMapper } from 'src/application'; +import { EntityNotFoundError } from 'src/application/application.errors'; import { CommodityRepositoryInterface } from 'src/application/interfaces'; import { EnsureOwnedSnapshotFn } from 'src/application/shared/ensureOwnedSnapshot'; import { Commodity, CommoditySnapshot } from 'src/domain/commodities'; @@ -28,6 +29,13 @@ export class UpdateCommodityUseCase { user, }); + if (commodityData.isTombstone) { + throw new EntityNotFoundError({ + entityId: commodityId, + entityType: Commodity.entityType, + }); + } + const commodity = Commodity.restore(commodityData); commodity.update(CommodityMapper.toUpdateProps(data)); diff --git a/apps/backend/src/db/schemas/index.ts b/apps/backend/src/db/schemas/index.ts index d5ada348..5b3ed92b 100644 --- a/apps/backend/src/db/schemas/index.ts +++ b/apps/backend/src/db/schemas/index.ts @@ -3,4 +3,4 @@ export { operationsTable, operationsRelations } from './operations'; export { transactionsTable, transactionsRelations } from './transactions'; export { settingsTable } from './settings'; export { usersTable } from './users'; -export { commoditiesTable } from './commodities'; +export { commoditiesTable, CommodityDbRow } from './commodities'; diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index 4bc1a439..88f2f473 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -37,6 +37,7 @@ import { OperationDbRow, CommodityDbInsert, commoditiesTable, + CommodityDbRow, } from './schema'; import * as schema from './schemas'; @@ -393,7 +394,7 @@ export class TestDB { precision?: CommodityPrecisionNumber; isTombstone?: boolean; }, - ) => { + ): Promise => { const nextName = this.commodityCounter.getNextName({ delimiter: '' }); const commodityData = { @@ -465,6 +466,45 @@ export class TestDB { return account; }; + // createCommodity = async ( + // userId: UUID, + // params?: { + // code?: CommodityCodeString; + // symbol?: CommoditySymbolString; + // name?: string; + // precision?: CommodityPrecisionNumber; + // isTombstone?: boolean; + // }, + // ) => { + // const nextName = this.commodityCounter.getNextName({ delimiter: '' }); + + // const commodityData = { + // code: params?.code ?? CommodityCode.create(`COM${nextName}`).valueOf(), + // isTombstone: params?.isTombstone ?? false, + // name: params?.name ?? `Commodity ${nextName}`, + // precision: params?.precision ?? 2, + // symbol: params?.symbol ?? `${nextName}`, + // userId, + // }; + + // const commodity = await this.db + // .insert(schema.commoditiesTable) + // .values({ + // code: commodityData.code, + // name: commodityData.name, + // precision: commodityData.precision, + // symbol: commodityData.symbol, + // userId: commodityData.userId, + // ...TestDB.createTimestamps, + // ...TestDB.uuid, + // isTombstone: commodityData.isTombstone, + // }) + // .returning() + // .get(); + + // return commodity; + // }; + insertCommodity = async (commodityData: CommodityDbInsert) => { const insertedCommodity = await this.db .insert(commoditiesTable) @@ -706,4 +746,12 @@ export class TestDB { }, }); }; + + getAllCommoditiesByUserId = async ( + userId: UUID, + ): Promise => { + return await this.db.query.commoditiesTable.findMany({ + where: eq(commoditiesTable.userId, userId), + }); + }; } diff --git a/apps/backend/src/db/test-utils/entityComparer.test.ts b/apps/backend/src/db/test-utils/entityComparer.test.ts new file mode 100644 index 00000000..7e6a881a --- /dev/null +++ b/apps/backend/src/db/test-utils/entityComparer.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest'; + +import { compareEntityArrays } from './entityComparer'; + +describe('compareEntityArrays', () => { + it('compares arrays by common fields and ignores actual extra fields', () => { + const expected = [ + { code: 'USD', name: 'US Dollar' }, + { code: 'EUR', name: 'Euro' }, + ]; + + const actual = [ + { code: 'EUR', id: 'eur-id', name: 'Euro' }, + { code: 'USD', id: 'usd-id', name: 'US Dollar' }, + ]; + + compareEntityArrays(expected, actual); + }); + + it('compares only specified fields', () => { + const expected = [ + { code: 'USD', name: 'Expected name' }, + { code: 'EUR', name: 'Expected name' }, + ]; + + const actual = [ + { code: 'EUR', name: 'Actual name' }, + { code: 'USD', name: 'Actual name' }, + ]; + + compareEntityArrays(expected, actual, { fields: ['code'] }); + }); + + it('excludes selected fields from comparison', () => { + const expected = [ + { code: 'USD', name: 'US Dollar', updatedAt: 'before' }, + { code: 'EUR', name: 'Euro', updatedAt: 'before' }, + ]; + + const actual = [ + { code: 'EUR', name: 'Euro', updatedAt: 'after' }, + { code: 'USD', name: 'US Dollar', updatedAt: 'after' }, + ]; + + compareEntityArrays(expected, actual, { exclude: ['updatedAt'] }); + }); + + it('compares different object types by shared fields', () => { + type ExpectedCommodity = { + code: string; + name: string; + }; + + type ActualCommodity = { + code: string; + id: string; + name: string; + userId: string; + }; + + const expected: ExpectedCommodity[] = [{ code: 'USD', name: 'US Dollar' }]; + const actual: ActualCommodity[] = [ + { + code: 'USD', + id: 'commodity-id', + name: 'US Dollar', + userId: 'user-id', + }, + ]; + + compareEntityArrays(expected, actual); + }); + + it('does not reuse the same actual item for multiple expected items', () => { + const expected = [ + { code: 'USD', name: 'US Dollar' }, + { code: 'USD', name: 'US Dollar' }, + ]; + + const actual = [ + { code: 'USD', name: 'US Dollar' }, + { code: 'EUR', name: 'Euro' }, + ]; + + expect(() => compareEntityArrays(expected, actual)).toThrow(); + }); +}); diff --git a/apps/backend/src/db/test-utils/entityComparer.ts b/apps/backend/src/db/test-utils/entityComparer.ts index b37de0bc..3c6ffc97 100644 --- a/apps/backend/src/db/test-utils/entityComparer.ts +++ b/apps/backend/src/db/test-utils/entityComparer.ts @@ -1,5 +1,17 @@ +import { isDeepStrictEqual } from 'node:util'; + import { expect } from 'vitest'; +type CommonKey = Extract; + +type CompareEntityArraysOptions< + TExpected extends object, + TActual extends object, +> = { + exclude?: CommonKey[]; + fields?: CommonKey[]; +}; + const omitKeys = ( obj: T, keysToOmit: (keyof T)[], @@ -11,6 +23,41 @@ const omitKeys = ( return result; }; +const pickKeys = ( + obj: T, + keys: TKey[], +): Pick => { + const result = {} as Pick; + + keys.forEach((key) => { + result[key] = obj[key]; + }); + + return result; +}; + +const getCommonKeys = ( + expected: TExpected, + actual: TActual, +): CommonKey[] => { + return (Object.keys(expected) as CommonKey[]).filter( + (key) => key in actual, + ); +}; + +const getComparableKeys = ( + expected: TExpected, + actual: TActual, + options: CompareEntityArraysOptions, +): CommonKey[] => { + const keys = options.fields ?? getCommonKeys(expected, actual); + const excludedKeys = new Set>( + options.exclude ?? [], + ); + + return keys.filter((key) => !excludedKeys.has(key)); +}; + export const compareEntities = ( before: T, after: T, @@ -36,6 +83,45 @@ export const compareCommonEntities = ( }); }; +export const compareEntityArrays = < + TExpected extends object, + TActual extends object, +>( + expectedItems: TExpected[], + actualItems: TActual[], + options: CompareEntityArraysOptions = {}, +) => { + expect(actualItems).toHaveLength(expectedItems.length); + + const matchedActualIndexes = new Set(); + + expectedItems.forEach((expectedItem) => { + const matchedIndex = actualItems.findIndex((actualItem, actualIndex) => { + if (matchedActualIndexes.has(actualIndex)) { + return false; + } + + const comparableKeys = getComparableKeys( + expectedItem, + actualItem, + options, + ); + + if (comparableKeys.length === 0) { + return false; + } + + return isDeepStrictEqual( + pickKeys(expectedItem, comparableKeys), + pickKeys(actualItem, comparableKeys), + ); + }); + + expect(matchedIndex).not.toBe(-1); + matchedActualIndexes.add(matchedIndex); + }); +}; + export const getEntityDiff = ( before: T, after: T, diff --git a/apps/backend/src/db/test-utils/index.ts b/apps/backend/src/db/test-utils/index.ts index 572fcfc0..cb791f8a 100644 --- a/apps/backend/src/db/test-utils/index.ts +++ b/apps/backend/src/db/test-utils/index.ts @@ -1,4 +1,5 @@ export { prettyPrint } from './prettyPrint'; +export { parseResponse } from './parseResponse'; export { OperationDataForTransaction, TransactionBuilder, @@ -7,4 +8,4 @@ export { TransactionPersistenceBuilderResult, TransactionRequestBuilderResult, } from './testEntityBuilder'; -export { compareEntities } from './entityComparer'; +export { compareEntities, compareEntityArrays } from './entityComparer'; diff --git a/apps/backend/src/db/test-utils/parseResponse.ts b/apps/backend/src/db/test-utils/parseResponse.ts new file mode 100644 index 00000000..6c32d9b5 --- /dev/null +++ b/apps/backend/src/db/test-utils/parseResponse.ts @@ -0,0 +1,3 @@ +export const parseResponse = (response: { body: string }): T => { + return JSON.parse(response.body) as T; +}; diff --git a/apps/backend/src/domain/commodities/commodity.entity.ts b/apps/backend/src/domain/commodities/commodity.entity.ts index 0b5ae208..a567c481 100644 --- a/apps/backend/src/domain/commodities/commodity.entity.ts +++ b/apps/backend/src/domain/commodities/commodity.entity.ts @@ -23,8 +23,8 @@ import type { CreateCommodityProps, } from './types'; -const DEFAULT_PRECISION = 2; -const DEFAULT_SYMBOL = null; +// const DEFAULT_PRECISION = 2; +// const DEFAULT_SYMBOL = null; export class Commodity { static readonly entityType = 'commodity'; @@ -39,6 +39,9 @@ export class Commodity { private precision: CommodityPrecisionNumber, ) {} + static readonly DEFAULT_PRECISION = 2; + static readonly DEFAULT_SYMBOL = null; + static create(user: User, props: CreateCommodityProps): Commodity { const identity = EntityIdentity.create(); const softDelete = SoftDelete.create(); @@ -50,10 +53,10 @@ export class Commodity { ); const parsedCommoditySymbol = parseCommoditySymbol( - props.symbol ?? DEFAULT_SYMBOL, + props.symbol ?? Commodity.DEFAULT_SYMBOL, ); const parsedCommodityPrecision = parseCommodityPrecision( - props.precision ?? DEFAULT_PRECISION, + props.precision ?? Commodity.DEFAULT_PRECISION, ); return new Commodity( diff --git a/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts b/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts index d4bf144f..80051583 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts @@ -1,11 +1,13 @@ import { ROUTES } from '@ledgerly/shared/routes'; -import { UUID } from '@ledgerly/shared/types'; +import { CommodityResponseDTO, UUID } from '@ledgerly/shared/types'; +import { CommodityDbRow } from 'src/db/schemas'; import { TestDB } from 'src/db/test-db'; -import { Id } from 'src/domain/domain-core'; +import { compareEntityArrays, parseResponse } from 'src/db/test-utils'; +import { Commodity } from 'src/domain'; +import { CommodityCode, Id } from 'src/domain/domain-core'; import { createServer } from 'src/presentation/http'; -import { beforeEach, describe, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars const url = `/api${ROUTES.commodities}`; const testUser = { @@ -17,10 +19,61 @@ const testUser = { describe('Commodities Integration Tests', () => { let testDB: TestDB; let server: ReturnType; - // eslint-disable-next-line @typescript-eslint/no-unused-vars let authToken: string; - // eslint-disable-next-line @typescript-eslint/no-unused-vars let userId: UUID; + let otherUserId: UUID; + + const activeCommodityData1 = { + code: CommodityCode.create('USD').valueOf(), + isTombstone: false, + name: 'US Dollar', + precision: 2, + symbol: '$', + }; + + const activeCommodityData2 = { + code: CommodityCode.create('EUR').valueOf(), + isTombstone: false, + name: 'Euro', + precision: 2, + symbol: '€', + }; + + const archivedCommodityData1 = { + code: CommodityCode.create('GBP').valueOf(), + isTombstone: true, + name: 'British Pound', + precision: 2, + symbol: '£', + }; + + const activeCommoditiesData = [activeCommodityData1, activeCommodityData2]; + const archivedCommoditiesData = [archivedCommodityData1]; + const allCommoditiesData = [ + ...activeCommoditiesData, + ...archivedCommoditiesData, + ]; + + let commoditiesDbRows: CommodityDbRow[]; + + type AuthorizedRequest = { + method: 'DELETE' | 'GET' | 'POST' | 'PUT'; + url: string; + body?: Record; + }; + + const injectWithToken = async (token: string, options: AuthorizedRequest) => { + return await server.inject({ + ...options, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + }; + + const injectAuthorized = async (options: AuthorizedRequest) => { + return await injectWithToken(authToken, options); + }; beforeEach(async () => { testDB = new TestDB(); @@ -40,79 +93,711 @@ describe('Commodities Integration Tests', () => { const decoded = server.jwt.decode(token) as unknown as { userId: UUID }; userId = Id.restore(decoded.userId).valueOf(); + + const otherUser = await testDB.createUser({ + email: 'other@example.com', + name: 'Other User', + password: 'Password123!', + }); + otherUserId = Id.restore(otherUser.id).valueOf(); + + commoditiesDbRows = await Promise.all([ + ...allCommoditiesData.map((commodity) => + testDB.createCommodity(userId, { + code: commodity.code, + isTombstone: commodity.isTombstone, + name: commodity.name, + precision: commodity.precision, + symbol: commodity.symbol, + }), + ), + ]); + }); + + afterEach(async () => { + await testDB.cleanupTestDb(); }); describe('GET /api/commodities', () => { - it.todo('should return all commodities for the authenticated user'); + it('should return all active commodities for the authenticated user when no status is provided', async () => { + const response = await injectAuthorized({ + method: 'GET', + url, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toHaveLength(activeCommoditiesData.length); + + compareEntityArrays(activeCommoditiesData, parsedResponse); + }); + + it('should return active commodities when status=active is provided', async () => { + const response = await injectAuthorized({ + method: 'GET', + url: `${url}?status=active`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toHaveLength(activeCommoditiesData.length); + + compareEntityArrays(activeCommoditiesData, parsedResponse); + }); + + it('should return archived commodities when status=archived is provided', async () => { + const response = await injectAuthorized({ + method: 'GET', + url: `${url}?status=archived`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toHaveLength(1); + + compareEntityArrays([archivedCommodityData1], parsedResponse); + }); + + it('should return all commodities when status=all is provided', async () => { + const response = await injectAuthorized({ + method: 'GET', + url: `${url}?status=all`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toHaveLength( + activeCommoditiesData.length + 1, // +1 for the archived commodity + ); + + compareEntityArrays( + [...activeCommoditiesData, archivedCommodityData1], + parsedResponse, + ); + }); + + it('should return 400 for invalid status query parameter', async () => { + const response = await injectAuthorized({ + method: 'GET', + url: `${url}?status=invalidStatus`, + }); + + expect(response.statusCode).toBe(400); + }); + + it('should not return commodities belonging to another user', async () => { + const otherUserCommodity = await testDB.createCommodity(otherUserId, { + code: CommodityCode.create('JPY').valueOf(), + isTombstone: false, + name: 'Japanese Yen', + precision: 2, + symbol: '¥', + }); + + const response = await injectAuthorized({ + method: 'GET', + url: `${url}?status=all`, + }); + + expect(response.statusCode).toBe(200); - it.todo('should not return commodities belonging to another user'); + const parsedResponse = parseResponse(response); - it.todo('should include archived commodities marked as archived'); + parsedResponse.forEach((commodity) => { + expect(commodity.code).not.toBe(otherUserCommodity.code); + }); - it.todo('should return 401 without authorization'); + expect(parsedResponse).toHaveLength(allCommoditiesData.length); + + compareEntityArrays(allCommoditiesData, parsedResponse); + }); + + it('should return 401 without authorization', async () => { + const response = await server.inject({ + method: 'GET', + url: `${url}?status=all`, + }); + + expect(response.statusCode).toBe(401); + }); }); describe('GET /api/commodities/:id', () => { - it.todo('should return a commodity by id for the authenticated user'); + it('should return a commodity by id for the authenticated user', async () => { + const testActiveCommodityDbRow = commoditiesDbRows[0]; + + const response = await injectAuthorized({ + method: 'GET', + url: `${url}/${testActiveCommodityDbRow.id}`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + code: testActiveCommodityDbRow.code, + isTombstone: testActiveCommodityDbRow.isTombstone, + name: testActiveCommodityDbRow.name, + precision: testActiveCommodityDbRow.precision, + symbol: testActiveCommodityDbRow.symbol, + }), + ); + }); + + it('should return an archived commodity by id marked as archived', async () => { + const testArchivedCommodityDbRow = commoditiesDbRows.find( + (commodity) => commodity.isTombstone, + ); + + if (!testArchivedCommodityDbRow) { + throw new Error('No archived commodity found in the test data.'); + } + + const response = await injectAuthorized({ + method: 'GET', + url: `${url}/${testArchivedCommodityDbRow.id}`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + code: testArchivedCommodityDbRow.code, + isTombstone: testArchivedCommodityDbRow.isTombstone, + name: testArchivedCommodityDbRow.name, + precision: testArchivedCommodityDbRow.precision, + symbol: testArchivedCommodityDbRow.symbol, + }), + ); + }); + + it('should return 404 when the commodity does not exist', async () => { + const response = await injectAuthorized({ + method: 'GET', + url: `${url}/${Id.create().valueOf()}`, // Random UUID that doesn't exist + }); - it.todo('should return an archived commodity by id marked as archived'); + expect(response.statusCode).toBe(404); + }); - it.todo('should return 404 when the commodity does not exist'); + it('should return 404 when the commodity belongs to another user', async () => { + const otherUserCommodity = await testDB.createCommodity(otherUserId, { + code: CommodityCode.create('JPY').valueOf(), + isTombstone: false, + name: 'Japanese Yen', + precision: 2, + symbol: '¥', + }); + + const response = await injectAuthorized({ + method: 'GET', + url: `${url}/${otherUserCommodity.id}`, + }); + + expect(response.statusCode).toBe(404); + }); + + it('should return 400 when the commodity id is invalid', async () => { + const response = await injectAuthorized({ + method: 'GET', + url: `${url}/invalid-id`, + }); + + expect(response.statusCode).toBe(400); + }); - it.todo('should return 404 when the commodity belongs to another user'); + it('should return 401 without authorization', async () => { + const testActiveCommodityDbRow = commoditiesDbRows[0]; - it.todo('should return 400 when the commodity id is invalid'); + const response = await server.inject({ + method: 'GET', + url: `${url}/${testActiveCommodityDbRow.id}`, + }); - it.todo('should return 401 without authorization'); + expect(response.statusCode).toBe(401); + }); }); describe('POST /api/commodities', () => { - it.todo('should create a commodity for the authenticated user'); + it('should create a commodity for the authenticated user', async () => { + const createCommodityData = { + code: CommodityCode.create('CHF').valueOf(), + name: 'Swiss Franc', + precision: 2, + symbol: 'CHF', + }; + + const response = await injectAuthorized({ + body: createCommodityData, + method: 'POST', + url, + }); + + expect(response.statusCode).toBe(201); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + ...createCommodityData, + isTombstone: false, + userId, + }), + ); + }); - it.todo('should create a commodity with default precision and null symbol'); + it('should create a commodity with default precision and null symbol', async () => { + const createCommodityData = { + code: CommodityCode.create('CAD').valueOf(), + name: 'Canadian Dollar', + }; + + const response = await injectAuthorized({ + body: createCommodityData, + method: 'POST', + url, + }); + + expect(response.statusCode).toBe(201); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + ...createCommodityData, + isTombstone: false, + precision: Commodity.DEFAULT_PRECISION, + symbol: Commodity.DEFAULT_SYMBOL, + userId, + }), + ); + }); - it.todo('should reject duplicate code for the same user'); + it('should reject duplicate code for the same user', async () => { + const response = await injectAuthorized({ + body: { + code: activeCommodityData1.code, + name: 'Duplicate US Dollar', + precision: 2, + symbol: '$', + }, + method: 'POST', + url, + }); + + expect(response.statusCode).toBe(409); + }); - it.todo('should allow the same code for different users'); + it('should allow the same code for different users', async () => { + const otherUserToken = server.jwt.sign({ + email: 'other@example.com', + userId: otherUserId, + }); + + const response = await injectWithToken(otherUserToken, { + body: { + code: activeCommodityData1.code, + name: 'Other User Dollar', + precision: 2, + symbol: '$', + }, + method: 'POST', + url, + }); + + expect(response.statusCode).toBe(201); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + code: activeCommodityData1.code, + userId: otherUserId, + }), + ); + }); - it.todo('should return 400 for invalid payload'); + it('should return 400 for invalid payload', async () => { + const response = await injectAuthorized({ + body: { + code: '1', + precision: 19, + symbol: 'symbol-is-too-long', + }, + method: 'POST', + url, + }); + + expect(response.statusCode).toBe(400); + }); - it.todo('should return 401 without authorization'); + it('should return 401 without authorization', async () => { + const response = await server.inject({ + body: { + code: CommodityCode.create('AUD').valueOf(), + name: 'Australian Dollar', + }, + method: 'POST', + url, + }); + + expect(response.statusCode).toBe(401); + }); }); describe('PUT /api/commodities/:id', () => { - it.todo('should update editable commodity metadata'); + it('should update editable commodity metadata', async () => { + const commodityToUpdate = commoditiesDbRows[0]; + + const updateData = { + code: CommodityCode.create('USDX').valueOf(), + name: 'Updated US Dollar', + symbol: 'US$', + }; + + const response = await injectAuthorized({ + body: updateData, + method: 'PUT', + url: `${url}/${commodityToUpdate.id}`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(typeof parsedResponse.updatedAt).toBe('string'); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + ...updateData, + createdAt: commodityToUpdate.createdAt, + id: commodityToUpdate.id, + isTombstone: false, + precision: commodityToUpdate.precision, + userId, + }), + ); + }); + + it('should reject duplicate code for the same user', async () => { + const commodityToUpdate = commoditiesDbRows[0]; + + const response = await injectAuthorized({ + body: { + code: activeCommodityData2.code, + }, + method: 'PUT', + url: `${url}/${commodityToUpdate.id}`, + }); + + expect(response.statusCode).toBe(409); + }); + + it('should allow updating to a code used by another user', async () => { + await testDB.createCommodity(otherUserId, { + code: CommodityCode.create('JPY').valueOf(), + name: 'Japanese Yen', + precision: 2, + symbol: '¥', + }); + + const commodityToUpdate = commoditiesDbRows[0]; + + const response = await injectAuthorized({ + body: { + code: CommodityCode.create('JPY').valueOf(), + }, + method: 'PUT', + url: `${url}/${commodityToUpdate.id}`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + code: CommodityCode.create('JPY').valueOf(), + id: commodityToUpdate.id, + userId, + }), + ); + }); + + it('should return 404 when updating another user commodity', async () => { + const otherUserCommodity = await testDB.createCommodity(otherUserId, { + code: CommodityCode.create('JPY').valueOf(), + name: 'Japanese Yen', + precision: 2, + symbol: '¥', + }); + + const response = await injectAuthorized({ + body: { + name: 'Updated Japanese Yen', + }, + method: 'PUT', + url: `${url}/${otherUserCommodity.id}`, + }); + + expect(response.statusCode).toBe(404); + }); + + it('should return 404 when updating an archived commodity', async () => { + const archivedCommodity = commoditiesDbRows.find( + (commodity) => commodity.isTombstone, + ); + + if (!archivedCommodity) { + throw new Error('No archived commodity found in the test data.'); + } + + const response = await injectAuthorized({ + body: { + name: 'Updated British Pound', + }, + method: 'PUT', + url: `${url}/${archivedCommodity.id}`, + }); + + expect(response.statusCode).toBe(404); + }); - it.todo('should reject duplicate code for the same user'); + it('should return 400 for invalid params', async () => { + const response = await injectAuthorized({ + body: { + name: 'Updated Commodity', + }, + method: 'PUT', + url: `${url}/invalid-id`, + }); - it.todo('should allow updating to a code used by another user'); + expect(response.statusCode).toBe(400); + }); + + it('should return 400 for invalid payload', async () => { + const commodityToUpdate = commoditiesDbRows[0]; + + const response = await injectAuthorized({ + body: { + code: '1', + precision: 3, + }, + method: 'PUT', + url: `${url}/${commodityToUpdate.id}`, + }); + + expect(response.statusCode).toBe(400); + }); + + it('should return 401 without authorization', async () => { + const commodityToUpdate = commoditiesDbRows[0]; + + const response = await server.inject({ + body: { + name: 'Updated Commodity', + }, + method: 'PUT', + url: `${url}/${commodityToUpdate.id}`, + }); - it.todo('should return 404 when updating another user commodity'); + expect(response.statusCode).toBe(401); + }); + + it("should not allow updating the commodity's precision", async () => { + const commodityToUpdate = commoditiesDbRows[0]; - it.todo('should return 404 when updating an archived commodity'); + const response = await injectAuthorized({ + body: { + precision: 5, + }, + method: 'PUT', + url: `${url}/${commodityToUpdate.id}`, + }); - it.todo('should return 400 for invalid params'); + expect(response.statusCode).toBe(200); - it.todo('should return 400 for invalid payload'); + const parsedResponse = parseResponse(response); - it.todo('should return 401 without authorization'); + expect(parsedResponse.precision).toBe(commodityToUpdate.precision); + }); }); describe('DELETE /api/commodities/:id', () => { - it.todo('should archive a commodity by id'); + it('should archive a commodity by id', async () => { + const commodityToArchive = commoditiesDbRows[0]; + + const response = await injectAuthorized({ + method: 'DELETE', + url: `${url}/${commodityToArchive.id}`, + }); + + expect(response.statusCode).toBe(204); + + const getResponse = await injectAuthorized({ + method: 'GET', + url: `${url}/${commodityToArchive.id}`, + }); + + expect(getResponse.statusCode).toBe(200); - it.todo('should leave the archived commodity readable by id'); + const parsedGetResponse = + parseResponse(getResponse); - it.todo( - 'should return the archived commodity in the list marked as archived', - ); + expect(parsedGetResponse).toEqual( + expect.objectContaining({ + id: commodityToArchive.id, + isTombstone: true, + }), + ); - it.todo('should return 404 when archiving another user commodity'); + const allCommodities = await testDB.getAllCommoditiesByUserId(userId); - it.todo('should return 404 when archiving an already archived commodity'); + const archivedCommoditiesDbRows = allCommodities.filter( + (commodity) => commodity.isTombstone, + ); - it.todo('should return 400 when the commodity id is invalid'); + const allActiveCommodities = allCommodities.filter( + (commodity) => !commodity.isTombstone, + ); - it.todo('should return 401 without authorization'); + expect(allActiveCommodities).toHaveLength( + activeCommoditiesData.length - 1, + ); + + expect(archivedCommoditiesDbRows).toHaveLength( + archivedCommoditiesData.length + 1, + ); + }); + + it('should leave the archived commodity readable by id', async () => { + const archivedCommodity = commoditiesDbRows.find( + (commodity) => commodity.isTombstone, + ); + + if (!archivedCommodity) { + throw new Error('No archived commodity found in the test data.'); + } + + const response = await injectAuthorized({ + method: 'GET', + url: `${url}/${archivedCommodity.id}`, + }); + + expect(response.statusCode).toBe(200); + + const parsedResponse = parseResponse(response); + + expect(parsedResponse).toEqual( + expect.objectContaining({ + id: archivedCommodity.id, + isTombstone: true, + }), + ); + }); + + it('should return the archived commodity in the list marked as archived', async () => { + const commodityToArchive = commoditiesDbRows[0]; + + const deleteResponse = await injectAuthorized({ + method: 'DELETE', + url: `${url}/${commodityToArchive.id}`, + }); + + expect(deleteResponse.statusCode).toBe(204); + + const listResponse = await injectAuthorized({ + method: 'GET', + url: `${url}?status=archived`, + }); + + expect(listResponse.statusCode).toBe(200); + + const parsedListResponse = + parseResponse(listResponse); + + expect(parsedListResponse).toHaveLength( + archivedCommoditiesData.length + 1, + ); + + compareEntityArrays( + [ + archivedCommodityData1, + { + code: commodityToArchive.code, + isTombstone: true, + name: commodityToArchive.name, + precision: commodityToArchive.precision, + symbol: commodityToArchive.symbol, + }, + ], + parsedListResponse, + ); + }); + + it('should return 404 when archiving another user commodity', async () => { + const otherUserCommodity = await testDB.createCommodity(otherUserId, { + code: CommodityCode.create('JPY').valueOf(), + name: 'Japanese Yen', + precision: 2, + symbol: '¥', + }); + + const response = await injectAuthorized({ + method: 'DELETE', + url: `${url}/${otherUserCommodity.id}`, + }); + + expect(response.statusCode).toBe(404); + }); + + it('should return 404 when archiving an already archived commodity', async () => { + const archivedCommodity = commoditiesDbRows.find( + (commodity) => commodity.isTombstone, + ); + + if (!archivedCommodity) { + throw new Error('No archived commodity found in the test data.'); + } + + const response = await injectAuthorized({ + method: 'DELETE', + url: `${url}/${archivedCommodity.id}`, + }); + + expect(response.statusCode).toBe(404); + }); + + it('should return 400 when the commodity id is invalid', async () => { + const response = await injectAuthorized({ + method: 'DELETE', + url: `${url}/invalid-id`, + }); + + expect(response.statusCode).toBe(400); + }); + + it('should return 401 without authorization', async () => { + const commodityToArchive = commoditiesDbRows[0]; + + const response = await server.inject({ + method: 'DELETE', + url: `${url}/${commodityToArchive.id}`, + }); + + expect(response.statusCode).toBe(401); + }); }); }); diff --git a/apps/backend/src/presentation/http/error-handler.ts b/apps/backend/src/presentation/http/error-handler.ts index a4421476..a4624c4e 100644 --- a/apps/backend/src/presentation/http/error-handler.ts +++ b/apps/backend/src/presentation/http/error-handler.ts @@ -5,7 +5,10 @@ import { type ValidationFieldErrorCode, } from '@ledgerly/shared/types'; import { FastifyError, FastifyReply, FastifyRequest } from 'fastify'; -import { DatabaseError } from 'src/infrastructure/errors'; +import { + DatabaseError, + RecordAlreadyExistsError, +} from 'src/infrastructure/errors'; import { isCodedError } from 'src/shared/errors'; import { reportDatabaseError } from 'src/shared/errors/reportDatabaseError'; import { ZodError, type ZodIssue } from 'zod'; @@ -108,6 +111,15 @@ export function errorHandler( ); } + if (error instanceof RecordAlreadyExistsError) { + return sendCodedError( + reply, + statusByErrorCode[apiErrorCodes.conflict], + apiErrorCodes.conflict, + {}, + ); + } + if (error instanceof DatabaseError) { reportDatabaseError(error); return sendCodedError( diff --git a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts index fe0e0166..581e850b 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts @@ -22,16 +22,12 @@ import { UserDbRow, } from 'src/db/schema'; import { CreateTransactionProps, TestDB } from 'src/db/test-db'; -import { compareEntities } from 'src/db/test-utils'; +import { compareEntities, parseResponse } from 'src/db/test-utils'; import { compareCommonEntities } from 'src/db/test-utils/entityComparer'; import { Amount, DateValue, Id } from 'src/domain/domain-core'; import { createServer } from 'src/presentation/http'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -const parseResponse = (response: { body: string }): T => { - return JSON.parse(response.body) as T; -}; - const testUser = { email: 'test@example.com', name: 'Test User', From d0f7b295f494925e41c2df1380b5f4e5bf5c3fca Mon Sep 17 00:00:00 2001 From: gorushkin Date: Mon, 27 Jul 2026 09:19:40 +0300 Subject: [PATCH 25/30] LED-110 feat(accounts, commodities): update create methods to include userId and adjust related tests --- .../interfaces/AccountRepository.interface.ts | 2 +- .../CommodityRepository.interface.ts | 5 +- .../accounts/__tests__/createAccount.test.ts | 31 +++--- .../usecases/accounts/createAccount.ts | 5 +- .../__tests__/createCommodity.test.ts | 25 ++--- .../usecases/commodities/createCommodity.ts | 5 +- .../db/accounts/account.repository.test.ts | 39 ++++++-- .../db/accounts/account.repository.ts | 9 +- .../commodities/commodity.repository.test.ts | 14 ++- .../db/commodities/commodity.repository.ts | 12 ++- .../http/accounts/account.integration.test.ts | 8 +- .../http/accounts/account.routes.ts | 3 +- .../commodities/commodity.controller.test.ts | 1 - .../commodities/commodity.integration.test.ts | 22 ++--- .../http/commodities/commodity.routes.ts | 3 +- docs/DOMAIN.md | 7 +- ...ity-registry-before-currency-validation.md | 15 ++- ...enforced-commodity-reference-validation.md | 7 ++ .../shared/src/validation/accounts.test.ts | 33 +++++++ packages/shared/src/validation/accounts.ts | 17 ++-- .../src/validation/baseValidations.test.ts | 27 +++++ .../shared/src/validation/baseValidations.ts | 17 ++-- packages/shared/src/validation/commodities.ts | 2 +- .../src/validation/transactions.test.ts | 99 +++++++++++++++++++ .../shared/src/validation/transactions.ts | 11 ++- 25 files changed, 329 insertions(+), 90 deletions(-) create mode 100644 packages/shared/src/validation/accounts.test.ts create mode 100644 packages/shared/src/validation/baseValidations.test.ts create mode 100644 packages/shared/src/validation/transactions.test.ts diff --git a/apps/backend/src/application/interfaces/AccountRepository.interface.ts b/apps/backend/src/application/interfaces/AccountRepository.interface.ts index a9abfdbf..b1af70a0 100644 --- a/apps/backend/src/application/interfaces/AccountRepository.interface.ts +++ b/apps/backend/src/application/interfaces/AccountRepository.interface.ts @@ -13,7 +13,7 @@ export type AccountRepositorySoftDeleteInput = Pick< export type AccountRepositoryInterface = { getAll(userId: UUID): Promise; - create(data: AccountSnapshot): Promise; + create(userId: UUID, data: AccountSnapshot): Promise; getById(userId: UUID, id: UUID): Promise; update( userId: UUID, diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts index d9c03b75..1a8bfe7b 100644 --- a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -15,7 +15,10 @@ export type CommodityRepositorySoftDeleteInput = Pick< export type CommodityRepositoryInterface = { getById(userId: UUID, commodityId: UUID): Promise; getAll(userId: UUID, status: QueryStatus): Promise; - create(commodity: CommoditySnapshot): Promise; + create( + userId: UUID, + commodity: CommoditySnapshot, + ): Promise; update( userId: UUID, commodityId: UUID, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts index 357391c1..05e255b7 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/createAccount.test.ts @@ -49,20 +49,23 @@ describe('CreateAccountUseCase', async () => { type, }); - expect(accountRepository.create).toHaveBeenCalledWith({ - commodityId: result.commodityId, - createdAt: result.createdAt, - currentClearedBalanceLocal, - description, - id: result.id, - initialBalance, - isSystem: false, - isTombstone: false, - name, - type, - updatedAt: result.updatedAt, - userId: result.userId, - }); + expect(accountRepository.create).toHaveBeenCalledWith( + user.getId().valueOf(), + { + commodityId: result.commodityId, + createdAt: result.createdAt, + currentClearedBalanceLocal, + description, + id: result.id, + initialBalance, + isSystem: false, + isTombstone: false, + name, + type, + updatedAt: result.updatedAt, + userId: result.userId, + }, + ); expect(result).toMatchObject({ description, diff --git a/apps/backend/src/application/usecases/accounts/createAccount.ts b/apps/backend/src/application/usecases/accounts/createAccount.ts index 838a963c..ada7e145 100644 --- a/apps/backend/src/application/usecases/accounts/createAccount.ts +++ b/apps/backend/src/application/usecases/accounts/createAccount.ts @@ -18,7 +18,10 @@ export class CreateAccountUseCase { const accountSnapshot = account.toSnapshot(); - await this.accountRepository.create(accountSnapshot); + await this.accountRepository.create( + user.getId().valueOf(), + accountSnapshot, + ); return AccountMapper.toResponseDTOFromSnapshot(accountSnapshot); } diff --git a/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts index eff2e419..5f668610 100644 --- a/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts +++ b/apps/backend/src/application/usecases/commodities/__tests__/createCommodity.test.ts @@ -40,17 +40,20 @@ describe('CreateCommodityUseCase', () => { const result = await createCommodityUseCase.execute(user, data); // Assert - expect(commodityRepository.create).toHaveBeenCalledWith({ - code: result.code, - createdAt: result.createdAt, - id: result.id, - isTombstone: false, - name: result.name, - precision: result.precision, - symbol: result.symbol, - updatedAt: result.updatedAt, - userId: result.userId, - }); + expect(commodityRepository.create).toHaveBeenCalledWith( + user.getId().valueOf(), + { + code: result.code, + createdAt: result.createdAt, + id: result.id, + isTombstone: false, + name: result.name, + precision: result.precision, + symbol: result.symbol, + updatedAt: result.updatedAt, + userId: result.userId, + }, + ); expect(result).toMatchObject({ code: data.code, diff --git a/apps/backend/src/application/usecases/commodities/createCommodity.ts b/apps/backend/src/application/usecases/commodities/createCommodity.ts index 42aaa7e5..bd512d88 100644 --- a/apps/backend/src/application/usecases/commodities/createCommodity.ts +++ b/apps/backend/src/application/usecases/commodities/createCommodity.ts @@ -22,7 +22,10 @@ export class CreateCommodityUseCase { const commoditySnapshot = commodity.toSnapshot(); - await this.commodityRepository.create(commoditySnapshot); + await this.commodityRepository.create( + user.getId().valueOf(), + commoditySnapshot, + ); return CommodityMapper.toResponseDTOFromSnapshot(commoditySnapshot); } diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts index 89fc2896..8d51aaf8 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts @@ -11,6 +11,7 @@ import { Id } from 'src/domain/domain-core/value-objects/Id'; import { AccountRepository } from 'src/infrastructure/db/'; import { RecordAlreadyExistsError, + RepositoryInvariantError, RepositoryNotFoundError, } from 'src/infrastructure/errors'; import { describe, beforeEach, it, expect, vi } from 'vitest'; @@ -93,7 +94,7 @@ describe('AccountRepository', () => { userId: user.id, }); - const account = await accountRepository.create({ + const account = await accountRepository.create(user.id, { ...newAccount, }); @@ -116,9 +117,27 @@ describe('AccountRepository', () => { userId: secondUser.id, }); - await expect(accountRepository.create(newAccount)).rejects.toThrowError( - RepositoryNotFoundError, - ); + await expect( + accountRepository.create(secondUser.id, newAccount), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + + it('should throw an invariant error when create userId differs from account snapshot userId', async () => { + const secondUser = await testDB.createUser({ + email: 'second-user@example.com', + name: 'Second User', + }); + + const newAccount = getAccountData({ + commodityId: usdCommodity.id, + name: 'New Account', + type: 'asset', + userId: secondUser.id, + }); + + await expect( + accountRepository.create(user.id, newAccount), + ).rejects.toThrowError(RepositoryInvariantError); }); it('should not allow duplicate account names for the same user', async () => { @@ -131,7 +150,9 @@ describe('AccountRepository', () => { await testDB.createAccount(user.id, usdCommodity.id, newAccount); - await expect(accountRepository.create(newAccount)).rejects.toThrowError( + await expect( + accountRepository.create(user.id, newAccount), + ).rejects.toThrowError( new RecordAlreadyExistsError({ context: { field: 'accountName', @@ -203,9 +224,9 @@ describe('AccountRepository', () => { userId: user.id, }); - await expect(accountRepository.create(newAccount)).rejects.toThrowError( - RepositoryNotFoundError, - ); + await expect( + accountRepository.create(user.id, newAccount), + ).rejects.toThrowError(RepositoryNotFoundError); }); }); @@ -524,7 +545,7 @@ describe('AccountRepository', () => { type: 'asset', userId: user.id, }); - const account = await accountRepository.create(newAccount); + const account = await accountRepository.create(user.id, newAccount); const accountDate = dayjs(account.createdAt); const afterCreate = dayjs(); diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.ts index f1bb4b50..5374b714 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.ts @@ -8,6 +8,7 @@ import { import { accountsTable } from 'src/db/schemas/accounts'; import { commoditiesTable } from 'src/db/schemas/commodities'; import { AccountSnapshot } from 'src/domain/accounts'; +import { RepositoryInvariantError } from 'src/infrastructure/errors'; import { BaseRepository } from '../BaseRepository'; @@ -36,9 +37,15 @@ export class AccountRepository }, 'Failed to fetch accounts'); } - create(data: AccountSnapshot): Promise { + create(userId: UUID, data: AccountSnapshot): Promise { return this.executeDatabaseOperation( async () => { + if (data.userId !== userId) { + throw new RepositoryInvariantError( + 'Account snapshot userId must match repository create userId', + ); + } + const existingCommodity = await this.db .select() .from(commoditiesTable) diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 10054119..88229628 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -187,8 +187,10 @@ describe('CommodityRepository', () => { userId: user.id, }; - const createdCommodity = - await commodityRepository.create(newCommodityData); + const createdCommodity = await commodityRepository.create( + user.id, + newCommodityData, + ); expect(createdCommodity).toMatchObject(newCommodityData); @@ -216,7 +218,7 @@ describe('CommodityRepository', () => { }; await expect( - commodityRepository.create(newCommodityData), + commodityRepository.create(user.id, newCommodityData), ).rejects.toThrowError( new RecordAlreadyExistsError({ context: { @@ -245,8 +247,10 @@ describe('CommodityRepository', () => { userId: anotherUser.id, }; - const createdCommodityForAnotherUser = - await commodityRepository.create(newCommodityData); + const createdCommodityForAnotherUser = await commodityRepository.create( + anotherUser.id, + newCommodityData, + ); expect(createdCommodityForAnotherUser).toMatchObject(newCommodityData); diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index 991bae72..c0d6ae7c 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -7,6 +7,7 @@ import { } from 'src/application'; import { commoditiesTable } from 'src/db/schemas/commodities'; import { CommoditySnapshot } from 'src/domain/commodities/types'; +import { RepositoryInvariantError } from 'src/infrastructure/errors'; import { BaseRepository } from '../BaseRepository'; @@ -58,9 +59,18 @@ export class CommodityRepository }, 'Failed to fetch commodities'); } - create(commodity: CommoditySnapshot): Promise { + create( + userId: UUID, + commodity: CommoditySnapshot, + ): Promise { return this.executeDatabaseOperation( async () => { + if (commodity.userId !== userId) { + throw new RepositoryInvariantError( + 'Commodity snapshot userId must match repository create userId', + ); + } + const commodityRow = CommodityPersistenceMapper.toDBRowFromSnapshot(commodity); diff --git a/apps/backend/src/presentation/http/accounts/account.integration.test.ts b/apps/backend/src/presentation/http/accounts/account.integration.test.ts index 4d93c82c..78ba19be 100644 --- a/apps/backend/src/presentation/http/accounts/account.integration.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.integration.test.ts @@ -196,7 +196,7 @@ describe('Accounts Integration Tests', () => { }); }); - describe('PUT /api/accounts/:id', () => { + describe('PATCH /api/accounts/:id', () => { it('should update an account by ID', async () => { const accountToUpdate = accounts[0]; @@ -208,7 +208,7 @@ describe('Accounts Integration Tests', () => { headers: { Authorization: `Bearer ${authToken}`, }, - method: 'PUT', + method: 'PATCH', payload: updatedData, url: `${url}/${accountToUpdate.id}`, }); @@ -266,8 +266,8 @@ describe('Accounts Integration Tests', () => { // - should return 400 when extra unexpected fields are provided }); - // Validation Tests for PUT /api/accounts/:id - describe.todo('PUT /api/accounts/:id - Validation', () => { + // Validation Tests for PATCH /api/accounts/:id + describe.todo('PATCH /api/accounts/:id - Validation', () => { // - should return 400 when name is empty string // - should return 400 when name is not a string // - should return 400 when type is invalid enum value diff --git a/apps/backend/src/presentation/http/accounts/account.routes.ts b/apps/backend/src/presentation/http/accounts/account.routes.ts index 57c34ae2..24c59397 100644 --- a/apps/backend/src/presentation/http/accounts/account.routes.ts +++ b/apps/backend/src/presentation/http/accounts/account.routes.ts @@ -30,8 +30,7 @@ export const accountsRoutes = (app: FastifyInstance) => { reply.status(204).send(); }); - // or patch - app.put('/:id', async (request, reply) => { + app.patch('/:id', async (request, reply) => { const user = request.user; const updatedAccount = await accountController.update( diff --git a/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts b/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts index 7d0b55a3..e3e00038 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.controller.test.ts @@ -81,7 +81,6 @@ describe('CommodityController', () => { ); const invalidQueryParams = [ - { invalidParam: 'invalidValue' }, { status: 'deleted' }, { status: '' }, { status: null }, diff --git a/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts b/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts index 80051583..95f4dc80 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.integration.test.ts @@ -57,7 +57,7 @@ describe('Commodities Integration Tests', () => { let commoditiesDbRows: CommodityDbRow[]; type AuthorizedRequest = { - method: 'DELETE' | 'GET' | 'POST' | 'PUT'; + method: 'DELETE' | 'GET' | 'PATCH' | 'POST'; url: string; body?: Record; }; @@ -457,7 +457,7 @@ describe('Commodities Integration Tests', () => { }); }); - describe('PUT /api/commodities/:id', () => { + describe('PATCH /api/commodities/:id', () => { it('should update editable commodity metadata', async () => { const commodityToUpdate = commoditiesDbRows[0]; @@ -469,7 +469,7 @@ describe('Commodities Integration Tests', () => { const response = await injectAuthorized({ body: updateData, - method: 'PUT', + method: 'PATCH', url: `${url}/${commodityToUpdate.id}`, }); @@ -498,7 +498,7 @@ describe('Commodities Integration Tests', () => { body: { code: activeCommodityData2.code, }, - method: 'PUT', + method: 'PATCH', url: `${url}/${commodityToUpdate.id}`, }); @@ -519,7 +519,7 @@ describe('Commodities Integration Tests', () => { body: { code: CommodityCode.create('JPY').valueOf(), }, - method: 'PUT', + method: 'PATCH', url: `${url}/${commodityToUpdate.id}`, }); @@ -548,7 +548,7 @@ describe('Commodities Integration Tests', () => { body: { name: 'Updated Japanese Yen', }, - method: 'PUT', + method: 'PATCH', url: `${url}/${otherUserCommodity.id}`, }); @@ -568,7 +568,7 @@ describe('Commodities Integration Tests', () => { body: { name: 'Updated British Pound', }, - method: 'PUT', + method: 'PATCH', url: `${url}/${archivedCommodity.id}`, }); @@ -580,7 +580,7 @@ describe('Commodities Integration Tests', () => { body: { name: 'Updated Commodity', }, - method: 'PUT', + method: 'PATCH', url: `${url}/invalid-id`, }); @@ -595,7 +595,7 @@ describe('Commodities Integration Tests', () => { code: '1', precision: 3, }, - method: 'PUT', + method: 'PATCH', url: `${url}/${commodityToUpdate.id}`, }); @@ -609,7 +609,7 @@ describe('Commodities Integration Tests', () => { body: { name: 'Updated Commodity', }, - method: 'PUT', + method: 'PATCH', url: `${url}/${commodityToUpdate.id}`, }); @@ -623,7 +623,7 @@ describe('Commodities Integration Tests', () => { body: { precision: 5, }, - method: 'PUT', + method: 'PATCH', url: `${url}/${commodityToUpdate.id}`, }); diff --git a/apps/backend/src/presentation/http/commodities/commodity.routes.ts b/apps/backend/src/presentation/http/commodities/commodity.routes.ts index dadafcb1..feb76f25 100644 --- a/apps/backend/src/presentation/http/commodities/commodity.routes.ts +++ b/apps/backend/src/presentation/http/commodities/commodity.routes.ts @@ -30,8 +30,7 @@ export const commodityRoutes = (app: FastifyInstance) => { reply.status(204).send(); }); - // or patch - app.put('/:id', async (request, reply) => { + app.patch('/:id', async (request, reply) => { const user = request.user; const updatedCommodity = await commodityController.update( diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index 16c825bd..e4c2fc6f 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -79,7 +79,12 @@ metadata owned by each user. See - Operations always store `amount` in the account's Commodity - Commodity `code`, `name`, and `symbol` are display metadata, not identity - Commodity `precision` defines integer minor-unit interpretation -- Commodity soft deletion is represented by technical `isTombstone` state +- Commodity archiving is represented by technical `isTombstone` state +- `GET /commodities` returns active Commodities by default and supports + `status=active|archived|all` +- `GET /commodities/:id` returns active and archived Commodities owned by the + authenticated user +- Archived Commodities remain readable but cannot be updated or archived again ## Entity API Conventions diff --git a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md index 2acf1aa1..d6a6d1d3 100644 --- a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md +++ b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md @@ -42,8 +42,8 @@ The Commodity model is: - `code` is unique only within one user's Commodity registry; - `precision` defines integer minor-unit interpretation and is immutable after Commodity creation in the MVP; -- Commodity supports technical soft deletion through `isTombstone` in the MVP; - user-facing archiving is out of scope for the initial Commodity registry. +- Commodity supports user-facing archiving through technical `isTombstone` + state. Domain references should move from currency strings to Commodity ids: @@ -60,6 +60,13 @@ The existing `/currencies` HTTP stub is not the target API and should be removed during the Commodity migration rather than implemented on top of the old model. +The public HTTP API exposes Commodity terminology through `/commodities`. +`GET /commodities` defaults to active Commodities and accepts +`status=active|archived|all`. `GET /commodities/:id` returns both active and +archived Commodities owned by the authenticated user. Archived Commodities remain +readable, but `PATCH /commodities/:id` and `DELETE /commodities/:id` operate only +on active Commodities. + The MVP explicitly excludes global reference assets, market data providers, exchange-rate history, automatic conversion, and network/contract metadata. Those can be introduced by later ADRs if needed. @@ -107,6 +114,9 @@ Those can be introduced by later ADRs if needed. clients migrate from currency strings to Commodity ids. - Amount validation should use Commodity precision rather than assuming all amounts have two decimal places. +- Commodity API clients can request active, archived, or all Commodities + explicitly. Archived Commodity records stay visible by id for audit and + reference continuity, but cannot be edited or archived again. ## Related @@ -117,6 +127,7 @@ Those can be introduced by later ADRs if needed. - [LED-66: Design Commodity schema and migration path](https://gorushkin.atlassian.net/browse/LED-66) - [LED-67: Implement Commodity persistence, repository, and defaults](https://gorushkin.atlassian.net/browse/LED-67) - [LED-68: Move Account to immutable Commodity reference](https://gorushkin.atlassian.net/browse/LED-68) +- [LED-110: Add Commodity use cases and HTTP API](https://gorushkin.atlassian.net/browse/LED-110) - [LED-111: Move transaction valuation to Commodity reference](https://gorushkin.atlassian.net/browse/LED-111) - [LED-69: Validate Commodity existence, ownership, and archive state](https://gorushkin.atlassian.net/browse/LED-69) - [LED-105: Remove `/currencies` endpoint stub](https://gorushkin.atlassian.net/browse/LED-105) diff --git a/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md b/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md index d59719f9..311a6fc2 100644 --- a/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md +++ b/docs/architecture/adr/0019-repository-enforced-commodity-reference-validation.md @@ -33,6 +33,11 @@ the referenced Commodity: - belongs to the same user as the write; - is not tombstoned. +Create repositories accept the authenticated `userId` as an explicit argument +and must reject snapshots whose `userId` differs from that argument. Snapshot +ownership remains domain state, but repository writes must not trust it as the +only authority for cross-user persistence boundaries. + Use cases remain responsible for request orchestration, aggregate construction, transaction boundaries and calling repository interfaces. They do not need to load the Commodity solely to prove that the persistence reference is valid. @@ -71,6 +76,8 @@ Commodity id has the expected UUID shape. Commodity existence/ownership/tombstone checks solely for reference integrity. - Account and transaction repositories must keep explicit pre-write checks for Commodity ownership and tombstone state. +- Create repositories must keep authenticated `userId` separate from snapshots + and fail fast on ownership mismatches before inserting rows. - Tests for missing, foreign and tombstoned Commodity references belong at the repository/write-boundary level, with higher-level tests added only where they verify public error mapping or user-visible behavior. diff --git a/packages/shared/src/validation/accounts.test.ts b/packages/shared/src/validation/accounts.test.ts new file mode 100644 index 00000000..5fced25c --- /dev/null +++ b/packages/shared/src/validation/accounts.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; + +import { accountCreateSchema, accountUpdateSchema } from "./accounts"; + +const validCreateAccount = { + commodityId: "00000000-0000-4000-8000-000000000000", + initialBalance: "0", + name: "Checking", + type: "asset", +}; + +describe("accountCreateSchema", () => { + it("defaults omitted description to an empty string", () => { + expect(accountCreateSchema.parse(validCreateAccount)).toEqual({ + ...validCreateAccount, + description: "", + }); + }); +}); + +describe("accountUpdateSchema", () => { + it("does not default omitted description", () => { + expect(accountUpdateSchema.parse({ name: "Checking" })).toEqual({ + name: "Checking", + }); + }); + + it("allows an explicit empty description to clear the field", () => { + expect(accountUpdateSchema.parse({ description: "" })).toEqual({ + description: "", + }); + }); +}); diff --git a/packages/shared/src/validation/accounts.ts b/packages/shared/src/validation/accounts.ts index 853da1a5..38de0b7c 100644 --- a/packages/shared/src/validation/accounts.ts +++ b/packages/shared/src/validation/accounts.ts @@ -3,8 +3,9 @@ import { z } from "zod"; import { ACCOUNT_TYPE_VALUES } from "../constants"; import { + defaultText, + optionalText, requiredText, - textWithDefault, uuid, amountString, } from "./baseValidations"; @@ -13,16 +14,14 @@ const accountType = z.enum(ACCOUNT_TYPE_VALUES); export const accountCreateSchema = z.object({ commodityId: uuid, - description: textWithDefault, + description: defaultText, initialBalance: amountString, name: requiredText, type: accountType, }); -export const accountUpdateSchema = accountCreateSchema - .pick({ - description: true, - name: true, - type: true, - }) - .partial(); +export const accountUpdateSchema = z.object({ + description: optionalText, + name: requiredText.optional(), + type: accountType.optional(), +}); diff --git a/packages/shared/src/validation/baseValidations.test.ts b/packages/shared/src/validation/baseValidations.test.ts new file mode 100644 index 00000000..ed84e72d --- /dev/null +++ b/packages/shared/src/validation/baseValidations.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { optionalText, requiredText } from "./baseValidations"; + +describe("requiredText", () => { + it("rejects empty text", () => { + expect(requiredText.safeParse("").success).toBe(false); + }); + + it("rejects whitespace-only text", () => { + expect(requiredText.safeParse(" ").success).toBe(false); + }); + + it("trims valid text", () => { + expect(requiredText.parse(" Ledgerly ")).toBe("Ledgerly"); + }); +}); + +describe("optionalText", () => { + it("keeps omitted text omitted", () => { + expect(optionalText.parse(undefined)).toBeUndefined(); + }); + + it("allows an explicit empty string", () => { + expect(optionalText.parse("")).toBe(""); + }); +}); diff --git a/packages/shared/src/validation/baseValidations.ts b/packages/shared/src/validation/baseValidations.ts index d1937b3b..7bf19222 100644 --- a/packages/shared/src/validation/baseValidations.ts +++ b/packages/shared/src/validation/baseValidations.ts @@ -10,8 +10,13 @@ import { DEFAULT_TRANSACTION_SORT_ORDER, } from "../constants/transactions"; -export const textWithDefault = z.string().default(""); -export const requiredText = z.string(); +export const text = z.string(); +export const optionalText = text.optional(); +export const defaultText = text.default(""); +export const requiredText = z + .string() + .trim() + .min(1, "Required text must not be empty"); export const updatedAt = z.string(); export const createdAt = z.string(); export const uuid = z.string().uuid().brand<"UUID">(); @@ -96,8 +101,6 @@ export const queryStatus = z .enum(["active", "archived", "all"]) .default("active"); -export const queryStatusSchema = z - .object({ - status: queryStatus, - }) - .strict(); +export const queryStatusSchema = z.object({ + status: queryStatus, +}); diff --git a/packages/shared/src/validation/commodities.ts b/packages/shared/src/validation/commodities.ts index 08e26543..063b39a5 100644 --- a/packages/shared/src/validation/commodities.ts +++ b/packages/shared/src/validation/commodities.ts @@ -11,7 +11,7 @@ export const commodityCreateSchema = z.object({ code: commodityCode, name: requiredText, precision: commodityPrecision.optional(), - symbol: commoditySymbol.nullable().optional(), + symbol: commoditySymbol.optional(), }); export const commodityUpdateSchema = commodityCreateSchema diff --git a/packages/shared/src/validation/transactions.test.ts b/packages/shared/src/validation/transactions.test.ts new file mode 100644 index 00000000..11e000a4 --- /dev/null +++ b/packages/shared/src/validation/transactions.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from "vitest"; + +import { + operationUpdateSchema, + transactionCreateSchema, + transactionUpdateSchema, +} from "./transactions"; + +const accountId = "00000000-0000-4000-8000-000000000001"; +const commodityId = "00000000-0000-4000-8000-000000000002"; +const operationId = "00000000-0000-4000-8000-000000000003"; + +const validOperationCreate = { + accountId, + amount: "100", + value: "100", +}; + +const validOperationUpdate = { + ...validOperationCreate, + description: "Updated operation", + id: operationId, +}; + +const validTransactionUpdate = { + description: "Updated transaction", + operations: { + create: [], + delete: [], + update: [validOperationUpdate], + }, + postingDate: "2026-07-27", + transactionDate: "2026-07-27", + version: 1, +}; + +describe("transactionCreateSchema", () => { + it("defaults omitted transaction and operation descriptions to empty strings", () => { + expect( + transactionCreateSchema.parse({ + commodityId, + operations: [ + validOperationCreate, + { + ...validOperationCreate, + amount: "-100", + value: "-100", + }, + ], + postingDate: "2026-07-27", + transactionDate: "2026-07-27", + }), + ).toMatchObject({ + description: "", + operations: [{ description: "" }, { description: "" }], + }); + }); +}); + +describe("transactionUpdateSchema", () => { + it("requires an explicit transaction description", () => { + const { description: _description, ...payload } = validTransactionUpdate; + + expect(transactionUpdateSchema.safeParse(payload).success).toBe(false); + }); + + it("requires an explicit updated operation description", () => { + const { description: _description, ...operation } = validOperationUpdate; + + expect( + operationUpdateSchema.safeParse({ + ...operation, + }).success, + ).toBe(false); + }); + + it("allows explicit empty descriptions to clear them", () => { + expect( + transactionUpdateSchema.parse({ + ...validTransactionUpdate, + description: "", + operations: { + ...validTransactionUpdate.operations, + update: [ + { + ...validOperationUpdate, + description: "", + }, + ], + }, + }), + ).toMatchObject({ + description: "", + operations: { + update: [{ description: "" }], + }, + }); + }); +}); diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index 364a5e35..a0bc8d51 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -7,7 +7,8 @@ import { import { uuid, - textWithDefault, + text, + defaultText, isoDate, amountString, } from "./baseValidations"; @@ -19,7 +20,7 @@ import { export const operationCreateSchema = z.object({ accountId: uuid, amount: amountString, - description: textWithDefault, + description: defaultText, value: amountString, }); @@ -27,14 +28,14 @@ export const operationCreateSchema = z.object({ export const operationUpdateSchema = z.object({ accountId: uuid, amount: amountString, - description: textWithDefault, + description: text, id: uuid, value: amountString, }); export const transactionCreateSchema = z.object({ commodityId: uuid, - description: textWithDefault, + description: defaultText, operations: z .array(operationCreateSchema) .min(MIN_TRANSACTION_OPERATIONS) @@ -44,7 +45,7 @@ export const transactionCreateSchema = z.object({ }); export const transactionUpdateSchema = z.object({ - description: textWithDefault, + description: text, operations: z.object({ create: z.array(operationCreateSchema).max(MAX_TRANSACTION_OPERATIONS), delete: z.array(uuid).max(MAX_TRANSACTION_OPERATIONS), From cd6c4227b997f1b7986219628aaa69d679191847 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Mon, 27 Jul 2026 09:31:11 +0300 Subject: [PATCH 26/30] LED-111 feat(docs): update transaction commodity references in documentation and API --- docs/DATABASE_SCHEMA.md | 13 ++++----- docs/DOMAIN.md | 9 ++++--- docs/MULTICURRENCY_DESIGN.md | 11 ++++---- ...ity-registry-before-currency-validation.md | 7 ++--- ...0007-no-operation-amount-sign-invariant.md | 10 +++---- .../adr/0014-keep-amount-value-object-name.md | 20 +++++++------- .../shared/src/validation/transactions.ts | 2 +- restAPI/transactions/create.bru | 25 +++++++++-------- .../createMultyCurrencyTransaction.bru | 25 +++++++++-------- .../createWithAccountNonExistError.bru | 26 +++++++++--------- restAPI/transactions/update.bru | 27 ++++++++++++------- 11 files changed, 96 insertions(+), 79 deletions(-) diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 27070ba2..fe6f02dc 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -58,7 +58,7 @@ erDiagram TRANSACTIONS { uuid id PK - uuid valuationCommodityId FK + uuid commodityId FK string description date transactionDate date postingDate @@ -211,7 +211,7 @@ Top-level grouping of related financial events. | Field | Type | Description | Constraints | |------|-----|----------|-------------| | `id` | UUID | Primary key | PK, NOT NULL | -| `valuationCommodityId` | UUID | Commodity that denominates operation `value` fields | FK -> `commodities.id`, NOT NULL | +| `commodityId` | UUID | Transaction Commodity that denominates operation `value` fields | FK -> `commodities.id`, NOT NULL | | `description` | String | Transaction description | NOT NULL | | `transactionDate` | Date | Transaction date | NOT NULL | | `postingDate` | Date | Posting date | NOT NULL | @@ -233,7 +233,8 @@ Top-level grouping of related financial events. **Notes:** -- `valuationCommodityId` determines the denomination of operation `value`. +- `commodityId` determines the transaction Commodity and denominates operation + `value`. - `version` supports optimistic concurrency control and increments on aggregate updates. @@ -249,7 +250,7 @@ Individual financial postings affecting accounts. | `transactionId` | UUID | Parent transaction | FK -> `transactions.id`, NOT NULL, ON DELETE CASCADE | | `accountId` | UUID | Affected account | FK -> `accounts.id`, NOT NULL, ON DELETE RESTRICT | | `amount` | Integer | Amount in the account's Commodity minor units | NOT NULL | -| `value` | Integer | Amount in the transaction valuation Commodity minor units | NOT NULL | +| `value` | Integer | Amount in the transaction Commodity minor units | NOT NULL | | `description` | String | Operation description | NOT NULL | | `isSystem` | Boolean | Reserved system operation flag | NOT NULL, default: false | | `isTombstone` | Boolean | Soft delete flag | NOT NULL, default: false | @@ -266,7 +267,7 @@ Individual financial postings affecting accounts. **Business Rules:** - `amount` is denominated by the operation account's Commodity. -- `value` is denominated by the parent transaction's valuation Commodity. +- `value` is denominated by the parent transaction's Commodity. - Balance check: `sum(value)` across active operations in a transaction must equal 0. - System operations are reserved for future trading-account support. @@ -349,7 +350,7 @@ USERS (root entity) 1. **Double-entry bookkeeping**: `sum(value)` across active operations in a transaction must equal 0. 2. **Commodity denomination**: `amount` is in account Commodity units; `value` - is in transaction valuation Commodity units. + is in transaction Commodity units. 3. **Immutable operations**: operations cannot be edited, only recreated on transaction updates. diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index e4c2fc6f..c502530e 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -31,7 +31,7 @@ Represents a single financial posting affecting an account. - Links to a transaction (`transactionId`) and an account (`accountId`) - Belongs to a user (`userId`) - `amount` — signed integer in the **account's Commodity** minor units -- `value` — signed integer in the **transaction valuation Commodity** minor units; used for balance validation +- `value` — signed integer in the **transaction Commodity** minor units; used for balance validation - `amount` and `value` must be valid integer minor-unit values. `NaN`, `Infinity`, missing values, and decimal/floating-point values are invalid. Zero is allowed and is not rejected by the domain model. @@ -75,7 +75,8 @@ metadata owned by each user. See - Each account has a designated Commodity - Account Commodity is immutable after account creation -- Transactions have a valuation Commodity that determines `value` denomination +- Transactions have `commodityId`, the transaction Commodity that determines + `value` denomination - Operations always store `amount` in the account's Commodity - Commodity `code`, `name`, and `symbol` are display metadata, not identity - Commodity `precision` defines integer minor-unit interpretation @@ -246,7 +247,7 @@ tasks. ### Commodity Handling -1. Each operation carries both `amount` (account Commodity) and `value` (transaction valuation Commodity) +1. Each operation carries both `amount` (account Commodity) and `value` (transaction Commodity) 2. For same-currency operations `amount === value` 3. **Trading operations** (`isSystem = true`) and system trading accounts are **not currently implemented**; they are reserved for a future multi-currency reconciliation phase @@ -366,7 +367,7 @@ Operation - transactionId: UUID (FK) -- directly linked to Transaction (Entry removed) - accountId: UUID (FK) - amount: integer -- in account Commodity minor units -- value: integer -- in transaction valuation Commodity minor units; used for balance validation +- value: integer -- in transaction Commodity minor units; used for balance validation - description: string (optional) - isSystem: boolean -- reserved for future trading operations (currently always false) - isTombstone: boolean diff --git a/docs/MULTICURRENCY_DESIGN.md b/docs/MULTICURRENCY_DESIGN.md index 39ec76c7..07f72333 100644 --- a/docs/MULTICURRENCY_DESIGN.md +++ b/docs/MULTICURRENCY_DESIGN.md @@ -135,7 +135,8 @@ Commodity identity is a stable id, not a display code. See ### Transaction Top-level entity representing a financial event: - `id`, `userId`, `description` -- `valuationCommodityId`, which determines `value` denomination +- `commodityId`, which determines the transaction Commodity and `value` + denomination - `transactionDate`, `postingDate` - `version` (optimistic concurrency) - `createdAt`, `updatedAt`, `isTombstone` @@ -144,7 +145,7 @@ Top-level entity representing a financial event: Individual account posting: - `id`, `transactionId`, `accountId`, `userId` - `amount` (signed integer, in the **account's Commodity**) -- `value` (signed integer, in the **transaction valuation Commodity**) +- `value` (signed integer, in the **transaction Commodity**) - `description` (optional) - `isSystem` (true for trading postings) - `createdAt`, `updatedAt`, `isTombstone` @@ -156,14 +157,14 @@ This follows the standard GnuCash split model: | Field | Denomination | Purpose | |-------|--------------|---------| | `amount` | Account's Commodity | How much was posted to this account | -| `value` | Transaction valuation Commodity | The equivalent amount in the transaction's denomination | +| `value` | Transaction Commodity | The equivalent amount in the transaction's denomination | For **same-currency** transactions both fields are equal. For **cross-currency** transactions they differ: ``` -Transaction currency: USD +Transaction Commodity: USD Operation on Assets:WalletRUB amount = -1000 (RUB — account's currency) value = -10 (USD — transaction's currency) @@ -252,7 +253,7 @@ Currently, Ledgerly stays closer to GnuCash than to PTA. - From `MIN_TRANSACTION_OPERATIONS` to `MAX_TRANSACTION_OPERATIONS` active operations per transaction (currently 2 to 1000) - Balance validated by `sum(value) = 0` across all operations - Immutable ledger operations -- `amount` = account currency; `value` = transaction currency (GnuCash convention) +- `amount` = account Commodity; `value` = transaction Commodity (GnuCash convention) ### Planned diff --git a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md index d6a6d1d3..cd2e855c 100644 --- a/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md +++ b/docs/architecture/adr/0006-commodity-registry-before-currency-validation.md @@ -50,9 +50,10 @@ Domain references should move from currency strings to Commodity ids: - `Account` references `commodityId`; - account Commodity is immutable after account creation; if the user selected the wrong Commodity, they create a new account; -- `Transaction` references `valuationCommodityId`; +- `Transaction` references `commodityId`, the transaction Commodity that + denominates operation `value`; - `Operation.amount` is denominated in the account Commodity; -- `Operation.value` is denominated in the transaction valuation Commodity; +- `Operation.value` is denominated in the transaction Commodity; - transaction balance validation continues to use the sum of active `Operation.value` values. @@ -107,7 +108,7 @@ Those can be introduced by later ADRs if needed. currency strings to per-user Commodity records. - Account creation must require a Commodity; account Commodity changes are not allowed after creation. -- Transaction creation must require a valuation Commodity. +- Transaction creation must require `commodityId`, the transaction Commodity. - The validation boundary for Commodity-backed account and transaction writes is defined separately in ADR 0019. - Existing API and DTO surfaces may need compatibility fields while data and diff --git a/docs/architecture/adr/0007-no-operation-amount-sign-invariant.md b/docs/architecture/adr/0007-no-operation-amount-sign-invariant.md index 1d3d1a97..278aa8ff 100644 --- a/docs/architecture/adr/0007-no-operation-amount-sign-invariant.md +++ b/docs/architecture/adr/0007-no-operation-amount-sign-invariant.md @@ -10,14 +10,14 @@ `LED-53` proposed requiring every transaction to contain at least one operation with a positive `amount` and one with a negative `amount`. -`amount` is denominated in an operation's account currency. Its sign represents +`amount` is denominated in an operation's account Commodity. Its sign represents the change to that particular account, not a universal debit or credit classification. The meaning of a debit or credit depends on account type, and -cross-currency transactions may use `amount` values that cannot be compared as +cross-Commodity transactions may use `amount` values that cannot be compared as a transaction-wide accounting invariant. The domain already validates the relevant transaction-level invariant: the sum -of operation `value` values in the transaction currency must equal zero. +of operation `value` values in the transaction Commodity must equal zero. ## Decision @@ -34,7 +34,7 @@ account types correctly. 1. Require both `amount` signs in every transaction - Pros: rejects a small class of suspicious operation sets. -- Cons: treats raw account-currency amounts as debit/credit semantics and can +- Cons: treats raw account-Commodity amounts as debit/credit semantics and can reject valid accounting shapes. 2. Derive debit and credit from account types @@ -54,7 +54,7 @@ account types correctly. - `LED-53` is not implemented as a domain validation rule. - No code or test changes are required for this decision. - Any later debit/credit validation must be designed around account types, - currencies, and a defined business use case. + Commodities, and a defined business use case. ## Related diff --git a/docs/architecture/adr/0014-keep-amount-value-object-name.md b/docs/architecture/adr/0014-keep-amount-value-object-name.md index caeb5b65..64e0f082 100644 --- a/docs/architecture/adr/0014-keep-amount-value-object-name.md +++ b/docs/architecture/adr/0014-keep-amount-value-object-name.md @@ -8,16 +8,16 @@ ## Context `Amount` represents a signed integer monetary magnitude in minor units without -currency. It is used for `Operation.amount`, `Operation.value`, account +Commodity identity. It is used for `Operation.amount`, `Operation.value`, account balances and test utilities. The name can be confused with the `Operation.amount` field, but the value object is intentionally broader than that field. `Operation.amount` is an -`Amount` denominated in the account currency, while `Operation.value` is an -`Amount` denominated in the transaction currency. +`Amount` denominated in the account Commodity, while `Operation.value` is an +`Amount` denominated in the transaction Commodity. Ledgerly previously had a separate `Money` value object for amount plus -currency semantics, but it was unused by the current domain model and removed +Commodity semantics, but it was unused by the current domain model and removed in LED-104. ## Decision @@ -25,8 +25,8 @@ in LED-104. Keep the value object named `Amount`. Document `Amount` as a signed integer minor-unit monetary amount without -currency. Use surrounding property names and domain context to explain the -currency denomination when needed. +Commodity identity. Use surrounding property names and domain context to +explain the Commodity denomination when needed. Do not rename `Amount` as part of domain invariant cleanup. @@ -36,7 +36,7 @@ Do not rename `Amount` as part of domain invariant cleanup. - Pros: highlights that the magnitude is monetary. - Cons: historically overlapped with `Money`, which meant amount plus - currency. + Commodity. 2. Rename to `MinorUnitAmount`. @@ -61,13 +61,13 @@ Positive: - Existing domain code keeps the short, established name. - `Amount` remains usable for both `Operation.amount` and `Operation.value`. -- The distinction between `Amount` and currency denomination stays explicit: +- The distinction between `Amount` and Commodity denomination stays explicit: `Amount` carries only the signed minor-unit magnitude, and surrounding domain - context supplies the currency. + context supplies the Commodity. Neutral/cost: -- Readers still need domain context to know which currency an `Amount` is +- Readers still need domain context to know which Commodity an `Amount` is denominated in. - `Operation.amount: Amount` keeps the field/type name overlap. diff --git a/packages/shared/src/validation/transactions.ts b/packages/shared/src/validation/transactions.ts index a0bc8d51..77b2662f 100644 --- a/packages/shared/src/validation/transactions.ts +++ b/packages/shared/src/validation/transactions.ts @@ -14,7 +14,7 @@ import { } from "./baseValidations"; // amount — posting in the account Commodity -// value — posting in the transaction valuation Commodity (GnuCash convention) +// value — posting in the transaction Commodity (GnuCash convention) // For same-currency transactions amount === value. // Transaction balance is validated by summing value across all operations (must equal 0). export const operationCreateSchema = z.object({ diff --git a/restAPI/transactions/create.bru b/restAPI/transactions/create.bru index e2825014..dfa7d422 100644 --- a/restAPI/transactions/create.bru +++ b/restAPI/transactions/create.bru @@ -20,18 +20,21 @@ auth:bearer { body:json { { + "commodityId": "{{commodityId}}", "description": "some transaction", - "entries": [ - [ - { - "accountId": "{{accountId1}}", - "amount": "-100" - }, - { - "accountId": "{{accountId2}}", - "amount": "100" - } - ] + "operations": [ + { + "accountId": "{{accountId1}}", + "amount": "-100", + "description": "Transfer from checking", + "value": "-100" + }, + { + "accountId": "{{accountId2}}", + "amount": "100", + "description": "Transfer to savings", + "value": "100" + } ], "postingDate": "2025-11-07", "transactionDate": "2025-11-07" diff --git a/restAPI/transactions/createMultyCurrencyTransaction.bru b/restAPI/transactions/createMultyCurrencyTransaction.bru index 5fd510a9..9553ac48 100644 --- a/restAPI/transactions/createMultyCurrencyTransaction.bru +++ b/restAPI/transactions/createMultyCurrencyTransaction.bru @@ -20,18 +20,21 @@ auth:bearer { body:json { { + "commodityId": "{{commodityId}}", "description": "MultiCurrency Transaction", - "entries": [ - [ - { - "accountId": "3141d189-4b2e-4af4-8b9c-f2ff0d7e0d72", - "amount": "-100" - }, - { - "accountId": "{{accountId2}}", - "amount": "100" - } - ] + "operations": [ + { + "accountId": "{{rubAccountId}}", + "amount": "-1000", + "description": "Spend RUB", + "value": "-10" + }, + { + "accountId": "{{usdAccountId}}", + "amount": "10", + "description": "Receive USD", + "value": "10" + } ], "postingDate": "2025-11-07", "transactionDate": "2025-11-07" diff --git a/restAPI/transactions/createWithAccountNonExistError.bru b/restAPI/transactions/createWithAccountNonExistError.bru index 774c517b..dddcd9f9 100644 --- a/restAPI/transactions/createWithAccountNonExistError.bru +++ b/restAPI/transactions/createWithAccountNonExistError.bru @@ -20,22 +20,20 @@ auth:bearer { body:json { { + "commodityId": "{{commodityId}}", "description": "some transaction", - "entries": [ + "operations": [ { - "description": "non-existent account", - "operations": [ - { - "accountId": "{{accountId1}}", - "amount": "-100", - "description": "Transfer from checking" - }, - { - "accountId": "{{accountId2}}", - "amount": "100", - "description": "Transfer to savings" - } - ] + "accountId": "{{nonExistentAccountId}}", + "amount": "-100", + "description": "Transfer from missing account", + "value": "-100" + }, + { + "accountId": "{{accountId2}}", + "amount": "100", + "description": "Transfer to savings", + "value": "100" } ], "postingDate": "2025-11-07", diff --git a/restAPI/transactions/update.bru b/restAPI/transactions/update.bru index 28b2d59a..4d0a9341 100644 --- a/restAPI/transactions/update.bru +++ b/restAPI/transactions/update.bru @@ -4,8 +4,8 @@ meta { seq: 5 } -post { - url: {{baseUrl}}/transactions +put { + url: {{baseUrl}}/transactions/{{transactionId}} body: json auth: bearer } @@ -20,20 +20,29 @@ auth:bearer { body:json { { - "description": "some transaction", - "entries": [ - [ + "description": "updated transaction", + "operations": { + "create": [], + "delete": [], + "update": [ { + "id": "{{operationId1}}", "accountId": "{{accountId1}}", - "amount": "-100" + "amount": "-100", + "description": "Updated transfer from checking", + "value": "-100" }, { + "id": "{{operationId2}}", "accountId": "{{accountId2}}", - "amount": "100" + "amount": "100", + "description": "Updated transfer to savings", + "value": "100" } ] - ], + }, "postingDate": "2025-11-07", - "transactionDate": "2025-11-07" + "transactionDate": "2025-11-07", + "version": {{transactionVersion}} } } From 11cc344e1bf8e41e9eb232d66e2d3ff16cd0ec1f Mon Sep 17 00:00:00 2001 From: gorushkin Date: Mon, 27 Jul 2026 10:45:08 +0300 Subject: [PATCH 27/30] LED-115 feat(accounts): prevent updates to tombstoned accounts --- .../db/accounts/account.repository.test.ts | 26 +++++++++++++++++++ .../db/accounts/account.repository.ts | 6 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts index 8d51aaf8..56cd39fe 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts @@ -1,10 +1,12 @@ import { apiErrorCodes, AccountTypeValue, UUID } from '@ledgerly/shared/types'; import dayjs from 'dayjs'; +import { eq } from 'drizzle-orm'; import { AccountDbInsert, AccountDbRow, CommodityDbRow, UserDbRow, + accountsTable, } from 'src/db/schema'; import { Amount, CommodityCode, Timestamp } from 'src/domain/domain-core'; import { Id } from 'src/domain/domain-core/value-objects/Id'; @@ -444,6 +446,30 @@ describe('AccountRepository', () => { expect(updatedSecondUserAccount?.name).toBe(accountData.name); }); + it('should throw RepositoryNotFoundError when account is tombstoned', async () => { + const tombstonedAccount = await testDB.createAccount( + user.id, + usdCommodity.id, + { + name: 'Tombstoned Account', + type: 'asset', + }, + ); + + await testDB.db + .update(accountsTable) + .set({ isTombstone: true }) + .where(eq(accountsTable.id, tombstonedAccount.id)); + + await expect( + accountRepository.update(user.id, tombstonedAccount.id, { + name: 'Updated Tombstoned Account', + type: 'expense', + updatedAt: Timestamp.create().valueOf(), + }), + ).rejects.toThrowError(RepositoryNotFoundError); + }); + it.todo('should preserve commodityId when updating account fields'); it('should only update allowed fields', async () => { diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.ts index 5374b714..8c9f80b1 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.ts @@ -134,7 +134,11 @@ export class AccountRepository .update(accountsTable) .set(safeData) .where( - and(eq(accountsTable.id, id), eq(accountsTable.userId, userId)), + and( + eq(accountsTable.id, id), + eq(accountsTable.userId, userId), + eq(accountsTable.isTombstone, false), + ), ) .returning() .get(); From 5657914bfa6d30e73dc80fb19eabb53a3b9f3ba9 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Mon, 27 Jul 2026 11:15:15 +0300 Subject: [PATCH 28/30] LED-116 feat(accounts, commodities): unify soft delete naming in repository and use cases --- .../interfaces/AccountRepository.interface.ts | 2 +- .../CommodityRepository.interface.ts | 2 +- ...Account.test.ts => archiveAccount.test.ts} | 18 +++++------ .../{deleteAccount.ts => archiveAccount.ts} | 4 +-- .../application/usecases/accounts/index.ts | 2 +- .../__tests__/archiveCommodity.test.ts | 8 ++--- .../usecases/commodities/archiveCommodity.ts | 2 +- apps/backend/src/di/container.ts | 6 ++-- apps/backend/src/di/types.ts | 4 +-- .../db/accounts/account.repository.test.ts | 30 ++++++++++++++----- .../db/accounts/account.repository.ts | 12 ++++++-- .../commodities/commodity.repository.test.ts | 22 +++++++------- .../db/commodities/commodity.repository.ts | 4 +-- .../http/accounts/account.controller.test.ts | 18 +++++------ .../http/accounts/account.controller.ts | 6 ++-- docs/DOMAIN.md | 4 +++ 16 files changed, 85 insertions(+), 59 deletions(-) rename apps/backend/src/application/usecases/accounts/__tests__/{deleteAccount.test.ts => archiveAccount.test.ts} (84%) rename apps/backend/src/application/usecases/accounts/{deleteAccount.ts => archiveAccount.ts} (86%) diff --git a/apps/backend/src/application/interfaces/AccountRepository.interface.ts b/apps/backend/src/application/interfaces/AccountRepository.interface.ts index b1af70a0..8edc522a 100644 --- a/apps/backend/src/application/interfaces/AccountRepository.interface.ts +++ b/apps/backend/src/application/interfaces/AccountRepository.interface.ts @@ -20,7 +20,7 @@ export type AccountRepositoryInterface = { id: UUID, data: AccountRepositoryUpdateInput, ): Promise; - delete( + softDelete( userId: UUID, id: UUID, data: AccountRepositorySoftDeleteInput, diff --git a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts index 1a8bfe7b..7ca70de9 100644 --- a/apps/backend/src/application/interfaces/CommodityRepository.interface.ts +++ b/apps/backend/src/application/interfaces/CommodityRepository.interface.ts @@ -24,7 +24,7 @@ export type CommodityRepositoryInterface = { commodityId: UUID, commodity: CommodityRepositoryUpdateInput, ): Promise; - delete( + softDelete( userId: UUID, commodityId: UUID, data: CommodityRepositorySoftDeleteInput, diff --git a/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts b/apps/backend/src/application/usecases/accounts/__tests__/archiveAccount.test.ts similarity index 84% rename from apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts rename to apps/backend/src/application/usecases/accounts/__tests__/archiveAccount.test.ts index a6ffa5e7..02a91271 100644 --- a/apps/backend/src/application/usecases/accounts/__tests__/deleteAccount.test.ts +++ b/apps/backend/src/application/usecases/accounts/__tests__/archiveAccount.test.ts @@ -6,16 +6,16 @@ import { Amount, Timestamp } from 'src/domain/domain-core'; import { Id } from 'src/domain/domain-core/value-objects/Id'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DeleteAccountUseCase } from '../deleteAccount'; +import { ArchiveAccountUseCase } from '../archiveAccount'; -describe('DeleteAccountUseCase', async () => { - let deleteAccountUseCase: DeleteAccountUseCase; +describe('ArchiveAccountUseCase', async () => { + let archiveAccountUseCase: ArchiveAccountUseCase; const user = await createUser(); let mockAccountRepository: { - delete: ReturnType; getById: ReturnType; + softDelete: ReturnType; }; let mockUserRepository: { getById: ReturnType }; @@ -58,15 +58,15 @@ describe('DeleteAccountUseCase', async () => { beforeEach(() => { mockAccountRepository = { - delete: vi.fn(), getById: vi.fn(), + softDelete: vi.fn(), }; mockUserRepository = { getById: vi.fn(), }; - deleteAccountUseCase = new DeleteAccountUseCase( + archiveAccountUseCase = new ArchiveAccountUseCase( mockAccountRepository as unknown as AccountRepositoryInterface, ); }); @@ -75,12 +75,12 @@ describe('DeleteAccountUseCase', async () => { it('should mark account as archived', async () => { mockUserRepository.getById.mockResolvedValue(mockUser); mockAccountRepository.getById.mockResolvedValue(mockAccountData); - mockAccountRepository.delete.mockResolvedValue(mockSavedAccountData); + mockAccountRepository.softDelete.mockResolvedValue(mockSavedAccountData); - const result = await deleteAccountUseCase.execute(user, accountId); + const result = await archiveAccountUseCase.execute(user, accountId); const { updatedAt: _updatedAt, ...expectedAccountData } = mockAccountData; - expect(mockAccountRepository.delete).toHaveBeenCalledWith( + expect(mockAccountRepository.softDelete).toHaveBeenCalledWith( user.getId().valueOf(), accountId, expect.objectContaining({ diff --git a/apps/backend/src/application/usecases/accounts/deleteAccount.ts b/apps/backend/src/application/usecases/accounts/archiveAccount.ts similarity index 86% rename from apps/backend/src/application/usecases/accounts/deleteAccount.ts rename to apps/backend/src/application/usecases/accounts/archiveAccount.ts index aece64fa..34bbd5e9 100644 --- a/apps/backend/src/application/usecases/accounts/deleteAccount.ts +++ b/apps/backend/src/application/usecases/accounts/archiveAccount.ts @@ -7,7 +7,7 @@ import { AccountRepositoryInterface } from '../../interfaces'; import { AccountUseCaseBase } from './accountBase'; -export class DeleteAccountUseCase extends AccountUseCaseBase { +export class ArchiveAccountUseCase extends AccountUseCaseBase { constructor(accountRepository: AccountRepositoryInterface) { super(accountRepository); } @@ -18,7 +18,7 @@ export class DeleteAccountUseCase extends AccountUseCaseBase { account.markAsDeleted(); - const deletedAccount = await this.accountRepository.delete( + const deletedAccount = await this.accountRepository.softDelete( user.getId().valueOf(), accountId, account.toSnapshot(), diff --git a/apps/backend/src/application/usecases/accounts/index.ts b/apps/backend/src/application/usecases/accounts/index.ts index 4f944e0f..abdee2d3 100644 --- a/apps/backend/src/application/usecases/accounts/index.ts +++ b/apps/backend/src/application/usecases/accounts/index.ts @@ -1,4 +1,4 @@ -export { DeleteAccountUseCase } from './deleteAccount'; +export { ArchiveAccountUseCase } from './archiveAccount'; export { CreateAccountUseCase } from './createAccount'; export { GetAccountByIdUseCase } from './getAccountById'; export { GetAllAccountsUseCase } from './getAllAccounts'; diff --git a/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts b/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts index b01989b4..ed2d00d2 100644 --- a/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts +++ b/apps/backend/src/application/usecases/commodities/__tests__/archiveCommodity.test.ts @@ -17,8 +17,8 @@ describe('ArchiveCommodityUseCase', () => { let user: User; const commodityRepository = { - delete: vi.fn(), getById: vi.fn(), + softDelete: vi.fn(), }; const archiveCommodityUseCase = new ArchiveCommodityUseCase( @@ -32,7 +32,7 @@ describe('ArchiveCommodityUseCase', () => { beforeEach(() => { vi.useRealTimers(); - commodityRepository.delete.mockClear(); + commodityRepository.softDelete.mockClear(); commodityRepository.getById.mockClear(); }); @@ -66,7 +66,7 @@ describe('ArchiveCommodityUseCase', () => { }; commodityRepository.getById.mockResolvedValue(commodity.toSnapshot()); - commodityRepository.delete.mockResolvedValue(archivedCommodityData); + commodityRepository.softDelete.mockResolvedValue(archivedCommodityData); vi.setSystemTime(new Date(timestampDuringUpdatingValue)); @@ -83,7 +83,7 @@ describe('ArchiveCommodityUseCase', () => { commodityId, ); - expect(commodityRepository.delete).toHaveBeenCalledWith( + expect(commodityRepository.softDelete).toHaveBeenCalledWith( userid, commodityId, { updatedAt: archivedCommodityData.timestamps.updatedAt }, diff --git a/apps/backend/src/application/usecases/commodities/archiveCommodity.ts b/apps/backend/src/application/usecases/commodities/archiveCommodity.ts index 13c5a133..c17862f9 100644 --- a/apps/backend/src/application/usecases/commodities/archiveCommodity.ts +++ b/apps/backend/src/application/usecases/commodities/archiveCommodity.ts @@ -32,7 +32,7 @@ export class ArchiveCommodityUseCase { commodity.markAsDeleted(); - const updatedCommodity = await this.commodityRepository.delete( + const updatedCommodity = await this.commodityRepository.softDelete( user.getId().valueOf(), commodityId, { updatedAt: commodity.getUpdatedAt().valueOf() }, diff --git a/apps/backend/src/di/container.ts b/apps/backend/src/di/container.ts index 7d3b36d4..0f401961 100644 --- a/apps/backend/src/di/container.ts +++ b/apps/backend/src/di/container.ts @@ -4,7 +4,7 @@ import { LoginUserUseCase, RegisterUserUseCase, CreateAccountUseCase, - DeleteAccountUseCase, + ArchiveAccountUseCase, GetAccountByIdUseCase, GetAllAccountsUseCase, UpdateAccountUseCase, @@ -84,7 +84,7 @@ export const createContainer = (db: DataBase): AppContainer => { const getAllAccountsUseCase = new GetAllAccountsUseCase(accountRepository); const getAccountByIdUseCase = new GetAccountByIdUseCase(accountRepository); const updateAccountUseCase = new UpdateAccountUseCase(accountRepository); - const deleteAccountUseCase = new DeleteAccountUseCase(accountRepository); + const archiveAccountUseCase = new ArchiveAccountUseCase(accountRepository); const loginUserUseCase = new LoginUserUseCase(userRepository); @@ -150,7 +150,7 @@ export const createContainer = (db: DataBase): AppContainer => { const useCases: AppContainer['useCases'] = { account: { - archiveAccount: deleteAccountUseCase, + archiveAccount: archiveAccountUseCase, createAccount: createAccountUseCase, getAccountById: getAccountByIdUseCase, getAllAccounts: getAllAccountsUseCase, diff --git a/apps/backend/src/di/types.ts b/apps/backend/src/di/types.ts index ccbe5f3a..472035b6 100644 --- a/apps/backend/src/di/types.ts +++ b/apps/backend/src/di/types.ts @@ -4,7 +4,7 @@ import { LoginUserUseCase, RegisterUserUseCase, CreateAccountUseCase, - DeleteAccountUseCase, + ArchiveAccountUseCase, GetAccountByIdUseCase, GetAllAccountsUseCase, UpdateAccountUseCase, @@ -51,7 +51,7 @@ type AccountUseCases = { getAllAccounts: GetAllAccountsUseCase; getAccountById: GetAccountByIdUseCase; updateAccount: UpdateAccountUseCase; - archiveAccount: DeleteAccountUseCase; + archiveAccount: ArchiveAccountUseCase; }; type AuthUseCases = { diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts index 56cd39fe..d2cf1300 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.test.ts @@ -494,20 +494,20 @@ describe('AccountRepository', () => { }); }); - describe('delete', () => { + describe('softDelete', () => { let account: AccountDbRow; beforeEach(async () => { account = await testDB.createAccount(user.id, usdCommodity.id); }); - it('should delete account when it exists and belongs to user', async () => { + it('should soft delete account when it exists and belongs to user', async () => { const deletedData = { isTombstone: true, updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }; - const deleted = await accountRepository.delete( + const deleted = await accountRepository.softDelete( user.id, account.id, deletedData, @@ -542,7 +542,7 @@ describe('AccountRepository', () => { }); await expect( - accountRepository.delete(secondUser.id, account.id, { + accountRepository.softDelete(secondUser.id, account.id, { updatedAt: Timestamp.create().valueOf(), }), ).rejects.toThrowError(RepositoryNotFoundError); @@ -553,12 +553,28 @@ describe('AccountRepository', () => { }); it('should throw RepositoryNotFoundError when account does not exist', async () => { - const result = accountRepository.delete(user.id, Id.create().valueOf(), { - updatedAt: Timestamp.create().valueOf(), - }); + const result = accountRepository.softDelete( + user.id, + Id.create().valueOf(), + { + updatedAt: Timestamp.create().valueOf(), + }, + ); await expect(result).rejects.toThrowError(RepositoryNotFoundError); }); + + it('should throw RepositoryNotFoundError when account is already tombstoned', async () => { + await accountRepository.softDelete(user.id, account.id, { + updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), + }); + + await expect( + accountRepository.softDelete(user.id, account.id, { + updatedAt: Timestamp.restore('2030-01-02T00:00:00.000Z').valueOf(), + }), + ).rejects.toThrowError(RepositoryNotFoundError); + }); }); describe('timestamp behavior', () => { diff --git a/apps/backend/src/infrastructure/db/accounts/account.repository.ts b/apps/backend/src/infrastructure/db/accounts/account.repository.ts index 8c9f80b1..c55f1e46 100644 --- a/apps/backend/src/infrastructure/db/accounts/account.repository.ts +++ b/apps/backend/src/infrastructure/db/accounts/account.repository.ts @@ -160,7 +160,7 @@ export class AccountRepository ); } - async delete( + async softDelete( userId: UUID, id: UUID, data: AccountRepositorySoftDeleteInput, @@ -169,7 +169,13 @@ export class AccountRepository const deletedAccount = await this.db .update(accountsTable) .set({ isTombstone: true, updatedAt: data.updatedAt }) - .where(and(eq(accountsTable.id, id), eq(accountsTable.userId, userId))) + .where( + and( + eq(accountsTable.id, id), + eq(accountsTable.userId, userId), + eq(accountsTable.isTombstone, false), + ), + ) .returning() .get(); @@ -180,7 +186,7 @@ export class AccountRepository ); return AccountPersistenceMapper.toSnapshot(existingAccount); - }, `Failed to delete account with ID ${id}`); + }, `Failed to soft delete account with ID ${id}`); } async ensureUserOwnsAccount(userId: UUID, accountId: UUID) { diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts index 88229628..fd867075 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.test.ts @@ -141,7 +141,7 @@ describe('CommodityRepository', () => { const commodity1 = await testDB.createCommodity(user.id); const commodity2 = await testDB.createCommodity(user.id); - await commodityRepository.delete(user.id, commodity1.id, { + await commodityRepository.softDelete(user.id, commodity1.id, { updatedAt: Timestamp.create().valueOf(), }); @@ -161,7 +161,7 @@ describe('CommodityRepository', () => { }); if (status === 'archived') { - await commodityRepository.delete(user.id, commodity1.id, { + await commodityRepository.softDelete(user.id, commodity1.id, { updatedAt: Timestamp.create().valueOf(), }); } @@ -373,7 +373,7 @@ describe('CommodityRepository', () => { }); it('should throw an error when trying to update tombstoned commodity', async () => { - await commodityRepository.delete(user.id, commodityDbRow.id, { + await commodityRepository.softDelete(user.id, commodityDbRow.id, { updatedAt: Timestamp.create().valueOf(), }); @@ -389,15 +389,15 @@ describe('CommodityRepository', () => { }); }); - describe('delete', () => { + describe('softDelete', () => { let commodityDbRow: CommodityDbRow; beforeEach(async () => { commodityDbRow = await testDB.createCommodity(user.id); }); - it('should delete the commodity when it exists', async () => { - await commodityRepository.delete(user.id, commodityDbRow.id, { + it('should soft delete the commodity when it exists', async () => { + await commodityRepository.softDelete(user.id, commodityDbRow.id, { updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }); @@ -413,7 +413,7 @@ describe('CommodityRepository', () => { const nonExistentId = Id.create().valueOf(); await expect( - commodityRepository.delete(user.id, nonExistentId, { + commodityRepository.softDelete(user.id, nonExistentId, { updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }), ).rejects.toThrowError(RepositoryNotFoundError); @@ -423,19 +423,19 @@ describe('CommodityRepository', () => { const anotherUser = await testDB.createUser(); await expect( - commodityRepository.delete(anotherUser.id, commodityDbRow.id, { + commodityRepository.softDelete(anotherUser.id, commodityDbRow.id, { updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }), ).rejects.toThrowError(RepositoryNotFoundError); }); - it('should throw an error when trying to delete a commodity that is already tombstoned', async () => { - await commodityRepository.delete(user.id, commodityDbRow.id, { + it('should throw an error when trying to soft delete a commodity that is already tombstoned', async () => { + await commodityRepository.softDelete(user.id, commodityDbRow.id, { updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }); await expect( - commodityRepository.delete(user.id, commodityDbRow.id, { + commodityRepository.softDelete(user.id, commodityDbRow.id, { updatedAt: Timestamp.restore('2030-01-01T00:00:00.000Z').valueOf(), }), ).rejects.toThrowError(RepositoryNotFoundError); diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index c0d6ae7c..e58fb5ea 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -139,7 +139,7 @@ export class CommodityRepository ); } - delete( + softDelete( userId: UUID, commodityId: UUID, data: CommodityRepositorySoftDeleteInput, @@ -165,6 +165,6 @@ export class CommodityRepository ); return CommodityPersistenceMapper.toSnapshot(existingCommodity); - }, 'Failed to delete commodity'); + }, 'Failed to soft delete commodity'); } } diff --git a/apps/backend/src/presentation/http/accounts/account.controller.test.ts b/apps/backend/src/presentation/http/accounts/account.controller.test.ts index 01f18bb3..5f5f77bd 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.test.ts @@ -1,6 +1,6 @@ import { AccountCreateDTO, AccountUpdateDTO } from '@ledgerly/shared/types'; import { - DeleteAccountUseCase, + ArchiveAccountUseCase, CreateAccountUseCase, GetAccountByIdUseCase, GetAllAccountsUseCase, @@ -38,7 +38,7 @@ describe('AccountController', () => { execute: vi.fn(), }; - const mockDeleteAccountUseCase = { + const mockArchiveAccountUseCase = { execute: vi.fn(), }; @@ -47,7 +47,7 @@ describe('AccountController', () => { mockGetAllAccountsUseCase as unknown as GetAllAccountsUseCase, mockCreateAccountUseCase as unknown as CreateAccountUseCase, mockUpdateAccountUseCase as unknown as UpdateAccountUseCase, - mockDeleteAccountUseCase as unknown as DeleteAccountUseCase, + mockArchiveAccountUseCase as unknown as ArchiveAccountUseCase, ); beforeEach(async () => { @@ -179,17 +179,17 @@ describe('AccountController', () => { }); }); - describe('delete', () => { - it('should call accountService.delete with correct id and userId', async () => { - mockDeleteAccountUseCase.execute.mockResolvedValue(undefined); + describe('deleteAccount', () => { + it('should call archive account use case with correct account id and user', async () => { + mockArchiveAccountUseCase.execute.mockResolvedValue(undefined); await accountController.deleteAccount(user, { id: accountId }); - expect(mockDeleteAccountUseCase.execute).toHaveBeenCalledWith( + expect(mockArchiveAccountUseCase.execute).toHaveBeenCalledWith( user, accountId, ); - expect(mockDeleteAccountUseCase.execute).toHaveBeenCalledTimes(1); + expect(mockArchiveAccountUseCase.execute).toHaveBeenCalledTimes(1); }); it('should throw ZodError for invalid request params', async () => { @@ -197,7 +197,7 @@ describe('AccountController', () => { accountController.deleteAccount(user, { id: 'not-a-uuid' }), ).rejects.toThrow(ZodError); - expect(mockDeleteAccountUseCase.execute).not.toHaveBeenCalled(); + expect(mockArchiveAccountUseCase.execute).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/backend/src/presentation/http/accounts/account.controller.ts b/apps/backend/src/presentation/http/accounts/account.controller.ts index 7d36f3b8..7a4aba6e 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.ts @@ -4,7 +4,7 @@ import { uniqueIdSchema, } from '@ledgerly/shared/validation'; import { - DeleteAccountUseCase, + ArchiveAccountUseCase, CreateAccountUseCase, GetAccountByIdUseCase, GetAllAccountsUseCase, @@ -18,7 +18,7 @@ export class AccountController { private readonly getAllAccountsUseCase: GetAllAccountsUseCase, private readonly createAccountUseCase: CreateAccountUseCase, private readonly updateAccountUseCase: UpdateAccountUseCase, - private readonly deleteAccountUseCase: DeleteAccountUseCase, + private readonly archiveAccountUseCase: ArchiveAccountUseCase, ) {} async getAll(user: User) { @@ -47,6 +47,6 @@ export class AccountController { async deleteAccount(user: User, requestParams: unknown) { const { id } = uniqueIdSchema.parse(requestParams); - return this.deleteAccountUseCase.execute(user, id); + return this.archiveAccountUseCase.execute(user, id); } } diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index c502530e..f6241392 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -121,6 +121,10 @@ Repositories persist timestamps received through snapshots/mappers. Soft-delete is a domain state transition, not an idempotent repository command. Calling `markAsDeleted()` on an already deleted entity must fail with `DELETED_ENTITY_OPERATION` and must not mutate `updatedAt` again. +Repository APIs that persist this transition must use `softDelete(...)`. +Application/use case APIs may use business-facing `archive...` names, and HTTP +routes may still expose `DELETE /resource/:id` commands. Repository +`delete(...)` is reserved for physical row deletion. Snapshot types live next to the entity in `domain//types.ts`. They use primitive/domain-safe fields and must not be aliases for DB rows or response From 7bc8c55b23acfe5e94bf363507c82b4072075f7c Mon Sep 17 00:00:00 2001 From: gorushkin Date: Mon, 27 Jul 2026 11:30:35 +0300 Subject: [PATCH 29/30] LED-116 chore(frontend): reset app to placeholder --- apps/frontend/README.md | 56 +- apps/frontend/eslint.config.js | 2 - apps/frontend/package.json | 17 +- apps/frontend/src/App.tsx | 12 + .../entities/accounts/api/accountActions.ts | 7 - .../entities/accounts/model/accountsState.ts | 80 --- .../accounts/ui/AccountsList/AccountsList.tsx | 60 -- .../accounts/ui/AccountsList/index.ts | 1 - .../currencies/api/currenciesActions.ts | 7 - .../frontend/src/entities/currencies/index.ts | 1 - .../currencies/model/currencyState.ts | 22 - .../transactions/api/transactionsActions.ts | 0 .../transactions/model/transactionsState.ts | 0 .../src/features/ConfirmDialog/index.ts | 2 - .../ConfirmDialog/model/confirmDialogState.ts | 37 - .../ConfirmDialog/ui/ConfirmDialog.tsx | 36 - .../ManageAccount/ui/ManageAccountForm.tsx | 157 ----- .../ManageAccount/ui/ManageAccountModal.tsx | 62 -- .../src/features/ManageAccount/ui/index.ts | 1 - apps/frontend/src/index.css | 87 ++- apps/frontend/src/main.tsx | 19 +- .../src/pages/accountPage/accountPage.tsx | 13 - apps/frontend/src/pages/accountPage/index.ts | 1 - .../src/pages/accountsPage/accountsPage.tsx | 8 - apps/frontend/src/pages/accountsPage/index.ts | 1 - apps/frontend/src/pages/indexPage/index.ts | 1 - .../src/pages/indexPage/indexPage.tsx | 8 - apps/frontend/src/routeTree.gen.ts | 222 ------ apps/frontend/src/routes/__root.tsx | 7 - apps/frontend/src/routes/about.tsx | 9 - .../src/routes/accounts/$accountId.tsx | 6 - apps/frontend/src/routes/accounts/index.tsx | 5 - apps/frontend/src/routes/budget.tsx | 9 - apps/frontend/src/routes/index.tsx | 6 - apps/frontend/src/routes/transactions.tsx | 9 - apps/frontend/src/shared/api/config.ts | 91 --- apps/frontend/src/shared/api/index.ts | 1 - apps/frontend/src/shared/config/config.ts | 5 - .../src/shared/lib/modalState/index.ts | 1 - .../src/shared/lib/modalState/modalState.ts | 17 - apps/frontend/src/shared/types/index.ts | 0 apps/frontend/src/shared/ui/Button/Button.tsx | 38 - apps/frontend/src/shared/ui/Button/index.ts | 1 - .../src/shared/ui/CloseButton/CloseButton.tsx | 13 - .../src/shared/ui/CloseButton/index.ts | 1 - .../src/shared/ui/Layout/Footer/Footer.tsx | 7 - .../src/shared/ui/Layout/Footer/index.ts | 1 - .../src/shared/ui/Layout/Header/Header.tsx | 21 - .../src/shared/ui/Layout/Header/index.ts | 1 - apps/frontend/src/shared/ui/Layout/Layout.tsx | 40 -- .../src/shared/ui/Layout/Main/Main.tsx | 11 - .../src/shared/ui/Layout/Main/index.ts | 1 - .../src/shared/ui/Layout/Sidebar/Sidebar.tsx | 17 - .../src/shared/ui/Layout/Sidebar/index.ts | 1 - apps/frontend/src/shared/ui/Layout/index.ts | 1 - apps/frontend/src/shared/ui/Modal/Modal.tsx | 121 ---- apps/frontend/src/shared/ui/Modal/index.ts | 0 apps/frontend/src/shared/ui/Modal/modal.css | 0 apps/frontend/src/shared/ui/Navbar/Navbar.tsx | 25 - apps/frontend/src/shared/ui/Navbar/index.ts | 1 - .../src/shared/ui/Navbar/navbarConfig.ts | 28 - .../src/widgets/Dashboard/Dashboard.tsx | 8 - apps/frontend/src/widgets/Dashboard/index.ts | 1 - apps/frontend/tsconfig.app.tsbuildinfo | 2 +- apps/frontend/vite.config.ts | 9 +- pnpm-lock.yaml | 660 +----------------- 66 files changed, 80 insertions(+), 2015 deletions(-) create mode 100644 apps/frontend/src/App.tsx delete mode 100644 apps/frontend/src/entities/accounts/api/accountActions.ts delete mode 100644 apps/frontend/src/entities/accounts/model/accountsState.ts delete mode 100644 apps/frontend/src/entities/accounts/ui/AccountsList/AccountsList.tsx delete mode 100644 apps/frontend/src/entities/accounts/ui/AccountsList/index.ts delete mode 100644 apps/frontend/src/entities/currencies/api/currenciesActions.ts delete mode 100644 apps/frontend/src/entities/currencies/index.ts delete mode 100644 apps/frontend/src/entities/currencies/model/currencyState.ts delete mode 100644 apps/frontend/src/entities/transactions/api/transactionsActions.ts delete mode 100644 apps/frontend/src/entities/transactions/model/transactionsState.ts delete mode 100644 apps/frontend/src/features/ConfirmDialog/index.ts delete mode 100644 apps/frontend/src/features/ConfirmDialog/model/confirmDialogState.ts delete mode 100644 apps/frontend/src/features/ConfirmDialog/ui/ConfirmDialog.tsx delete mode 100644 apps/frontend/src/features/ManageAccount/ui/ManageAccountForm.tsx delete mode 100644 apps/frontend/src/features/ManageAccount/ui/ManageAccountModal.tsx delete mode 100644 apps/frontend/src/features/ManageAccount/ui/index.ts delete mode 100644 apps/frontend/src/pages/accountPage/accountPage.tsx delete mode 100644 apps/frontend/src/pages/accountPage/index.ts delete mode 100644 apps/frontend/src/pages/accountsPage/accountsPage.tsx delete mode 100644 apps/frontend/src/pages/accountsPage/index.ts delete mode 100644 apps/frontend/src/pages/indexPage/index.ts delete mode 100644 apps/frontend/src/pages/indexPage/indexPage.tsx delete mode 100644 apps/frontend/src/routeTree.gen.ts delete mode 100644 apps/frontend/src/routes/__root.tsx delete mode 100644 apps/frontend/src/routes/about.tsx delete mode 100644 apps/frontend/src/routes/accounts/$accountId.tsx delete mode 100644 apps/frontend/src/routes/accounts/index.tsx delete mode 100644 apps/frontend/src/routes/budget.tsx delete mode 100644 apps/frontend/src/routes/index.tsx delete mode 100644 apps/frontend/src/routes/transactions.tsx delete mode 100644 apps/frontend/src/shared/api/config.ts delete mode 100644 apps/frontend/src/shared/api/index.ts delete mode 100644 apps/frontend/src/shared/config/config.ts delete mode 100644 apps/frontend/src/shared/lib/modalState/index.ts delete mode 100644 apps/frontend/src/shared/lib/modalState/modalState.ts delete mode 100644 apps/frontend/src/shared/types/index.ts delete mode 100644 apps/frontend/src/shared/ui/Button/Button.tsx delete mode 100644 apps/frontend/src/shared/ui/Button/index.ts delete mode 100644 apps/frontend/src/shared/ui/CloseButton/CloseButton.tsx delete mode 100644 apps/frontend/src/shared/ui/CloseButton/index.ts delete mode 100644 apps/frontend/src/shared/ui/Layout/Footer/Footer.tsx delete mode 100644 apps/frontend/src/shared/ui/Layout/Footer/index.ts delete mode 100644 apps/frontend/src/shared/ui/Layout/Header/Header.tsx delete mode 100644 apps/frontend/src/shared/ui/Layout/Header/index.ts delete mode 100644 apps/frontend/src/shared/ui/Layout/Layout.tsx delete mode 100644 apps/frontend/src/shared/ui/Layout/Main/Main.tsx delete mode 100644 apps/frontend/src/shared/ui/Layout/Main/index.ts delete mode 100644 apps/frontend/src/shared/ui/Layout/Sidebar/Sidebar.tsx delete mode 100644 apps/frontend/src/shared/ui/Layout/Sidebar/index.ts delete mode 100644 apps/frontend/src/shared/ui/Layout/index.ts delete mode 100644 apps/frontend/src/shared/ui/Modal/Modal.tsx delete mode 100644 apps/frontend/src/shared/ui/Modal/index.ts delete mode 100644 apps/frontend/src/shared/ui/Modal/modal.css delete mode 100644 apps/frontend/src/shared/ui/Navbar/Navbar.tsx delete mode 100644 apps/frontend/src/shared/ui/Navbar/index.ts delete mode 100644 apps/frontend/src/shared/ui/Navbar/navbarConfig.ts delete mode 100644 apps/frontend/src/widgets/Dashboard/Dashboard.tsx delete mode 100644 apps/frontend/src/widgets/Dashboard/index.ts diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 14478299..16b0be9f 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,56 +1,16 @@ # Ledgerly Frontend -React-based frontend application for personal finance management. +This package is intentionally reduced to a placeholder React app. -## Tech Stack - -- **React 19** with TypeScript -- **TanStack Router** for routing -- **MobX** for state management -- **React Hook Form** for form handling -- **Tailwind CSS + DaisyUI** for styling -- **Vite** for build tooling - -## Architecture - -### Project Structure -``` -src/ - entities/ # Business entities and models - features/ # Feature-specific components - pages/ # Page components - routes/ # Route definitions (TanStack Router) - shared/ # Shared utilities and components - widgets/ # Complex reusable components -``` - -### State Management -- **MobX** stores for reactive state management -- **mobx-react-lite** for React integration -- Stores organized by domain entities - -### Routing -- File-based routing with TanStack Router -- Type-safe navigation -- Automatic route generation +The previous frontend was removed because it drifted from the current backend +domain model. New frontend work should start from `src/App.tsx` and add +frameworks, routing, state management, and API clients back only when they are +needed. ## Development ```bash -# Install dependencies -pnpm install - -# Start development server -pnpm --filter frontend dev - -# Build for production -pnpm --filter frontend build +pnpm --filter @ledgerly/frontend dev +pnpm --filter @ledgerly/frontend build +pnpm --filter @ledgerly/frontend ts-check ``` - -## Key Features - -- Account management (Asset, Liability, Income, Expense) -- Transaction tracking with split entries -- Multi-currency support -- Income/Expense account-based classification -- User profile management diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js index fe92d685..357ba227 100644 --- a/apps/frontend/eslint.config.js +++ b/apps/frontend/eslint.config.js @@ -4,7 +4,6 @@ import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; import tseslint from 'typescript-eslint'; import react from 'eslint-plugin-react'; -import pluginRouter from '@tanstack/eslint-plugin-router'; import { baseConfig, commonIgnores } from '../../eslint.base.config.js'; export default tseslint.config( @@ -15,7 +14,6 @@ export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked, - ...pluginRouter.configs['flat/recommended'], ], settings: { react: { diff --git a/apps/frontend/package.json b/apps/frontend/package.json index b9a60c4a..cefcfc4b 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -12,28 +12,13 @@ "ts-check": "tsc -b --noEmit" }, "dependencies": { - "@hookform/resolvers": "^5.0.1", - "@ledgerly/shared": "workspace:*", - "@tailwindcss/vite": "^4.1.5", - "@tanstack/react-router": "^1.119.0", - "@tanstack/react-router-devtools": "^1.119.1", - "clsx": "^2.1.1", - "lucide-react": "^0.507.0", - "mobx": "^6.13.7", - "mobx-react-lite": "^4.1.0", "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-hook-form": "^7.56.2", - "tailwind-merge": "^3.3.0", - "tailwindcss": "^4.1.5" + "react-dom": "^19.0.0" }, "devDependencies": { - "@tanstack/eslint-plugin-router": "^1.115.0", - "@tanstack/router-plugin": "^1.119.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", - "daisyui": "^5.0.35", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx new file mode 100644 index 00000000..7a470b0e --- /dev/null +++ b/apps/frontend/src/App.tsx @@ -0,0 +1,12 @@ +export const App = () => ( +
+
+

Ledgerly frontend

+

Frontend rebuild in progress

+

+ The previous UI was removed because it no longer matches the backend domain model. A new frontend will be built + here from scratch. +

+
+
+); diff --git a/apps/frontend/src/entities/accounts/api/accountActions.ts b/apps/frontend/src/entities/accounts/api/accountActions.ts deleted file mode 100644 index e5ffcae8..00000000 --- a/apps/frontend/src/entities/accounts/api/accountActions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ROUTES } from '@ledgerly/shared/routes'; -import { AccountCreateDTO, AccountResponseDTO } from '@ledgerly/shared/types'; -import { baseActions } from 'src/shared/api'; - -const URL = ROUTES.accounts; - -export const accountActions = baseActions(URL); diff --git a/apps/frontend/src/entities/accounts/model/accountsState.ts b/apps/frontend/src/entities/accounts/model/accountsState.ts deleted file mode 100644 index e8d2725b..00000000 --- a/apps/frontend/src/entities/accounts/model/accountsState.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AccountResponseDTO, AccountCreateDTO } from '@ledgerly/shared/types'; -import { makeAutoObservable, runInAction } from 'mobx'; -import { ModalState } from 'src/shared/lib/modalState'; - -import { accountActions } from '../api/accountActions'; - -class AccountsState { - data: AccountResponseDTO[] = []; - currentAccount: AccountResponseDTO | null = null; - - modalState = new ModalState(); - constructor() { - makeAutoObservable(this); - } - - resetCurrentAccount = () => { - this.currentAccount = null; - }; - - getAll = async () => { - const response = await accountActions.read(); - - runInAction(() => { - if (response.ok) { - this.data = response.data; - } else { - console.error(response.error); - } - }); - }; - - create = async (account: AccountCreateDTO) => { - const response = await accountActions.create(account); - - runInAction(() => { - if (response.ok) { - this.data.push(response.data); - } else { - console.error(response.error); - } - }); - }; - - update = async (id: string, account: AccountCreateDTO) => { - const response = await accountActions.update(id, account); - - runInAction(() => { - if (response.ok) { - const index = this.data.findIndex((a) => a.id === id); - if (index !== -1) { - this.data[index] = response.data; - } - } else { - console.error(response.error); - } - }); - }; - - delete = async (id: string) => { - const response = await accountActions.delete(id); - - runInAction(() => { - if (response.ok) { - this.data = this.data.filter((a) => a.id !== id); - } else { - console.error(response.error); - } - }); - }; - - getById = (id: string) => { - const account = this.data.find((account) => account.id === id); - if (account) { - this.currentAccount = account; - return; - } - }; -} - -export const accountsState = new AccountsState(); diff --git a/apps/frontend/src/entities/accounts/ui/AccountsList/AccountsList.tsx b/apps/frontend/src/entities/accounts/ui/AccountsList/AccountsList.tsx deleted file mode 100644 index cf941a55..00000000 --- a/apps/frontend/src/entities/accounts/ui/AccountsList/AccountsList.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { JSX, useEffect } from 'react'; - -import { AccountResponseDTO } from '@ledgerly/shared/types'; -import { Link } from '@tanstack/react-router'; -import { Wallet, Banknote, CreditCard, Pencil } from 'lucide-react'; -import { observer } from 'mobx-react-lite'; -import { Button } from 'src/shared/ui/Button'; - -import { accountsState } from '../../model/accountsState'; - -const iconClassName = 'w-5 h-5'; - -const accountIcons: Record = { - bank: , - cash: , - credit: , - default: , -}; - -const getAccountIcon = (type: string) => accountIcons[type] || accountIcons.default; - -type AccountListItemProps = { - account: AccountResponseDTO; -}; - -const AccountListItem = ({ account }: AccountListItemProps) => { - const handleClick = () => { - accountsState.getById(account.id); - accountsState.modalState.open(); - }; - - return ( -
  • - - {getAccountIcon(account.type)} - {account.name} - - -
  • - ); -}; - -export const AccountsList = observer(() => { - useEffect(() => { - void accountsState.getAll(); - }, []); - - return ( -
    -

    Accounts List

    -
      - {accountsState.data.map((account) => ( - - ))} -
    -
    - ); -}); diff --git a/apps/frontend/src/entities/accounts/ui/AccountsList/index.ts b/apps/frontend/src/entities/accounts/ui/AccountsList/index.ts deleted file mode 100644 index 5b177876..00000000 --- a/apps/frontend/src/entities/accounts/ui/AccountsList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AccountsList } from './AccountsList'; diff --git a/apps/frontend/src/entities/currencies/api/currenciesActions.ts b/apps/frontend/src/entities/currencies/api/currenciesActions.ts deleted file mode 100644 index 63206ad1..00000000 --- a/apps/frontend/src/entities/currencies/api/currenciesActions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ROUTES } from '@ledgerly/shared/routes'; -import { Currency } from '@ledgerly/shared/types'; -import { baseActions } from 'src/shared/api'; - -const URL = ROUTES.currencies; - -export const currencyActions = baseActions(URL); diff --git a/apps/frontend/src/entities/currencies/index.ts b/apps/frontend/src/entities/currencies/index.ts deleted file mode 100644 index 046ed878..00000000 --- a/apps/frontend/src/entities/currencies/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { currencyState } from './model/currencyState'; diff --git a/apps/frontend/src/entities/currencies/model/currencyState.ts b/apps/frontend/src/entities/currencies/model/currencyState.ts deleted file mode 100644 index eafc113d..00000000 --- a/apps/frontend/src/entities/currencies/model/currencyState.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Currency } from '@ledgerly/shared/types'; -import { runInAction } from 'mobx'; - -import { currencyActions } from '../api/currenciesActions'; - -class CurrencyState { - currencies: Currency[] = []; - - getAll = async () => { - const response = await currencyActions.read(); - - runInAction(() => { - if (response.ok) { - this.currencies = response.data; - } else { - console.error(response.error); - } - }); - }; -} - -export const currencyState = new CurrencyState(); diff --git a/apps/frontend/src/entities/transactions/api/transactionsActions.ts b/apps/frontend/src/entities/transactions/api/transactionsActions.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/frontend/src/entities/transactions/model/transactionsState.ts b/apps/frontend/src/entities/transactions/model/transactionsState.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/frontend/src/features/ConfirmDialog/index.ts b/apps/frontend/src/features/ConfirmDialog/index.ts deleted file mode 100644 index 5f4e7848..00000000 --- a/apps/frontend/src/features/ConfirmDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConfirmDialog } from './ui/ConfirmDialog'; -export { confirmDialogState } from './model/confirmDialogState'; diff --git a/apps/frontend/src/features/ConfirmDialog/model/confirmDialogState.ts b/apps/frontend/src/features/ConfirmDialog/model/confirmDialogState.ts deleted file mode 100644 index bbe86c84..00000000 --- a/apps/frontend/src/features/ConfirmDialog/model/confirmDialogState.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { makeAutoObservable } from 'mobx'; - -type ConfirmDialogConfig = { - title: string; - message: string; - onConfirm: () => void; - cancelText?: string; - confirmText?: string; -}; - -class ConfirmDialogState { - isOpen = false; - config: ConfirmDialogConfig | null = null; - - constructor() { - makeAutoObservable(this); - } - - open = (config: ConfirmDialogConfig) => { - this.config = config; - this.isOpen = true; - }; - - close = () => { - this.isOpen = false; - this.config = null; - }; - - confirm = () => { - if (this.config?.onConfirm) { - this.config.onConfirm(); - } - this.close(); - }; -} - -export const confirmDialogState = new ConfirmDialogState(); diff --git a/apps/frontend/src/features/ConfirmDialog/ui/ConfirmDialog.tsx b/apps/frontend/src/features/ConfirmDialog/ui/ConfirmDialog.tsx deleted file mode 100644 index d541cedd..00000000 --- a/apps/frontend/src/features/ConfirmDialog/ui/ConfirmDialog.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { AlertTriangle } from 'lucide-react'; -import { observer } from 'mobx-react-lite'; -import { Button } from 'src/shared/ui/Button'; -import { Modal } from 'src/shared/ui/Modal/Modal'; - -import { confirmDialogState } from '../model/confirmDialogState'; - -export const ConfirmDialog = observer(() => { - const { close, config, confirm, isOpen } = confirmDialogState; - - if (!config) return null; - - const { cancelText = 'Отмена', confirmText = 'Удалить', message, title } = config; - - return ( - - -
    - -

    {title}

    -
    -
    - -

    {message}

    -
    - - - - -
    - ); -}); diff --git a/apps/frontend/src/features/ManageAccount/ui/ManageAccountForm.tsx b/apps/frontend/src/features/ManageAccount/ui/ManageAccountForm.tsx deleted file mode 100644 index 7e6dd822..00000000 --- a/apps/frontend/src/features/ManageAccount/ui/ManageAccountForm.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { ACCOUNT_TYPES } from '@ledgerly/shared/constants'; -import { AccountType, Currency } from '@ledgerly/shared/types'; -import { AccountCreateDTO } from '@ledgerly/shared/types'; -import { accountCreateSchema } from '@ledgerly/shared/validation'; -import { observer } from 'mobx-react-lite'; -import { useForm } from 'react-hook-form'; -import { accountsState } from 'src/entities/accounts/model/accountsState'; -import { currencyState } from 'src/entities/currencies'; - -type ManageAccountFormProps = { - formId: string; -}; - -// TODO: Move to constants -const optionMapping: Record = { - cash: 'Наличные', - credit: 'Кредитная карта', - debit: 'Дебетовая карта', - investment: 'Инвестиционный счет', - savings: 'Сберегательный счет', -}; - -// TODO: Move to constants -const currencyMapping: Record = { - AUD: 'Австралийский доллар', - CAD: 'Канадский доллар', - CHF: 'Швейцарский франк', - CNY: 'Китайский юань', - EUR: 'Евро', - GBP: 'Британский фунт стерлингов', - INR: 'Индийская рупия', - JPY: 'Японская иена', - RUB: 'Российский рубль', - USD: 'Доллар США', -}; - -const options = Object.entries(optionMapping).map(([value, label]) => ({ - label, - value, -})); - -const CURRENCIES = Object.entries(currencyMapping).map(([code, name]) => ({ - code, - name, -})); - -export const ManageAccountForm = observer((props: ManageAccountFormProps) => { - const { formId } = props; - const { currentAccount, modalState } = accountsState; - - const CURRENCY_TYPES = currencyState.currencies; - - const isEditMode = Boolean(currentAccount); - - const { - formState: { errors }, - handleSubmit, - register, - } = useForm({ - resolver: zodResolver(accountCreateSchema), - values: { - currencyCode: currentAccount?.currencyCode ?? CURRENCY_TYPES[0].code, - description: currentAccount?.description ?? '', - initialBalance: 0, - name: currentAccount?.name ?? '', - type: currentAccount?.type ?? ACCOUNT_TYPES[0], - }, - }); - - const handleClose = () => { - accountsState.resetCurrentAccount(); - modalState.close(); - }; - - const handleFormSubmit = (data: AccountCreateDTO) => { - try { - if (currentAccount) { - void accountsState.update(currentAccount.id, data); - } else { - void accountsState.create(data); - } - handleClose(); - } catch (error) { - console.error('Failed to save account:', error); - } - }; - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - void handleSubmit(handleFormSubmit)(e); - }; - - return ( -
    -
    - - - {errors.name && {errors.name.message}} -
    - -
    - - - {errors.description && {errors.description.message}} -
    - -
    - - - {errors.type && {errors.type.message}} -
    - -
    - - - {errors.currencyCode && {errors.currencyCode.message}} -
    - - {!isEditMode && ( -
    - - - {errors.initialBalance && {errors.initialBalance.message}} -
    - )} -
    - ); -}); diff --git a/apps/frontend/src/features/ManageAccount/ui/ManageAccountModal.tsx b/apps/frontend/src/features/ManageAccount/ui/ManageAccountModal.tsx deleted file mode 100644 index 8359605e..00000000 --- a/apps/frontend/src/features/ManageAccount/ui/ManageAccountModal.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { accountsState } from 'src/entities/accounts/model/accountsState'; -import { confirmDialogState } from 'src/features/ConfirmDialog'; -import { Button } from 'src/shared/ui/Button'; -import { Modal } from 'src/shared/ui/Modal/Modal'; - -import { ManageAccountForm } from './ManageAccountForm'; - -export const ManageAccountModal = observer(() => { - const { currentAccount, modalState } = accountsState; - const isEditMode = Boolean(currentAccount); - - const FORM_ID = 'manage-account-form'; - - if (!modalState.isOpen) { - return null; - } - - const title = currentAccount ? 'Редактировать счет' : 'Добавить новый счет'; - - const handleClose = () => { - accountsState.resetCurrentAccount(); - modalState.close(); - }; - - const onDeleteClick = () => { - if (!currentAccount) return; - - confirmDialogState.open({ - message: `Вы уверены, что хотите удалить счет "${currentAccount.name}"?`, - onConfirm: () => { - void accountsState.delete(currentAccount.id); - handleClose(); - }, - title: 'Удалить счет', - }); - }; - - return ( - - - - - - - {isEditMode && ( - - )} -
    - - -
    -
    -
    - ); -}); diff --git a/apps/frontend/src/features/ManageAccount/ui/index.ts b/apps/frontend/src/features/ManageAccount/ui/index.ts deleted file mode 100644 index 98c83de2..00000000 --- a/apps/frontend/src/features/ManageAccount/ui/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ManageAccountModal } from './ManageAccountModal'; diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css index 36205b34..c38f457b 100644 --- a/apps/frontend/src/index.css +++ b/apps/frontend/src/index.css @@ -1,52 +1,63 @@ -@import 'tailwindcss'; - -@plugin "daisyui" { - themes: corporate --default; -} - -@source inline('btn btn-neutral btn-primary btn-secondary btn-accent btn-info btn-success btn-warning btn-error btn-outline btn-dash btn-soft btn-ghost btn-link btn-active btn-disabled btn-xs btn-sm btn-md btn-lg btn-xl btn-wide btn-block btn-square btn-circle'); - -/* DaisyUI plugin should be configured in the Tailwind CSS configuration file */ - -@layer components { - .g { - outline: 1px solid green; - } - - .r { - outline: 1px solid red; - } - - .b { - outline: 1px solid blue; - } - - .y { - outline: 1px solid yellow; - } -} - :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; + color: #17202a; + background: #f6f7f9; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", sans-serif; font-weight: 400; - color: black; - - background-color: #242424; - + line-height: 1.5; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} - width: 100%; +* { + box-sizing: border-box; } body { margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; - position: relative; +} + +button, +input, +textarea, +select { + font: inherit; +} + +.app-shell { + display: grid; + min-height: 100vh; + place-items: center; + padding: 24px; +} + +.placeholder { + max-width: 560px; + width: 100%; +} + +.eyebrow { + color: #4f46e5; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + margin: 0 0 12px; + text-transform: uppercase; +} + +h1 { + font-size: clamp(2rem, 4vw, 3.5rem); + line-height: 1.05; + margin: 0 0 16px; +} + +.placeholder p:last-child { + color: #4b5563; + font-size: 1.0625rem; + margin: 0; } diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 183c8cdb..c80cf205 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -1,24 +1,9 @@ import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; -import { RouterProvider, createRouter } from '@tanstack/react-router'; - import './index.css'; +import { App } from './App'; -// Import the generated route tree -import { routeTree } from './routeTree.gen'; - -// Create a new router instance -const router = createRouter({ routeTree }); - -// Register the router instance for type safety -declare module '@tanstack/react-router' { - interface Register { - router: typeof router; - } -} - -// Render the app const rootElement = document.getElementById('root')!; if (!rootElement.innerHTML) { @@ -26,7 +11,7 @@ if (!rootElement.innerHTML) { root.render( - + ); } diff --git a/apps/frontend/src/pages/accountPage/accountPage.tsx b/apps/frontend/src/pages/accountPage/accountPage.tsx deleted file mode 100644 index 9fad848c..00000000 --- a/apps/frontend/src/pages/accountPage/accountPage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useParams } from '@tanstack/react-router'; - -export const AccountPage = () => { - const { accountId } = useParams({ strict: false }); - - return ( -
    -

    Welcome to the Account Page

    -

    This is the account page of the application.

    -

    Account ID: {accountId}

    -
    - ); -}; diff --git a/apps/frontend/src/pages/accountPage/index.ts b/apps/frontend/src/pages/accountPage/index.ts deleted file mode 100644 index f9fb654b..00000000 --- a/apps/frontend/src/pages/accountPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AccountPage } from './accountPage'; diff --git a/apps/frontend/src/pages/accountsPage/accountsPage.tsx b/apps/frontend/src/pages/accountsPage/accountsPage.tsx deleted file mode 100644 index 8e1f3406..00000000 --- a/apps/frontend/src/pages/accountsPage/accountsPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const AccountsPage = () => { - return ( -
    -

    Accounts

    -

    List of accounts will be displayed here.

    -
    - ); -}; diff --git a/apps/frontend/src/pages/accountsPage/index.ts b/apps/frontend/src/pages/accountsPage/index.ts deleted file mode 100644 index c3680ccd..00000000 --- a/apps/frontend/src/pages/accountsPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AccountsPage } from './accountsPage'; diff --git a/apps/frontend/src/pages/indexPage/index.ts b/apps/frontend/src/pages/indexPage/index.ts deleted file mode 100644 index b7d9ae89..00000000 --- a/apps/frontend/src/pages/indexPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IndexPage } from './indexPage'; diff --git a/apps/frontend/src/pages/indexPage/indexPage.tsx b/apps/frontend/src/pages/indexPage/indexPage.tsx deleted file mode 100644 index 3ccf7fd9..00000000 --- a/apps/frontend/src/pages/indexPage/indexPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const IndexPage = () => { - return ( -
    -

    Welcome to the Index Page

    -

    This is the main page of the application.

    -
    - ); -}; diff --git a/apps/frontend/src/routeTree.gen.ts b/apps/frontend/src/routeTree.gen.ts deleted file mode 100644 index 090c36db..00000000 --- a/apps/frontend/src/routeTree.gen.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -// Import Routes - -import { Route as rootRoute } from './routes/__root' -import { Route as TransactionsImport } from './routes/transactions' -import { Route as BudgetImport } from './routes/budget' -import { Route as AboutImport } from './routes/about' -import { Route as IndexImport } from './routes/index' -import { Route as AccountsIndexImport } from './routes/accounts/index' -import { Route as AccountsAccountIdImport } from './routes/accounts/$accountId' - -// Create/Update Routes - -const TransactionsRoute = TransactionsImport.update({ - id: '/transactions', - path: '/transactions', - getParentRoute: () => rootRoute, -} as any) - -const BudgetRoute = BudgetImport.update({ - id: '/budget', - path: '/budget', - getParentRoute: () => rootRoute, -} as any) - -const AboutRoute = AboutImport.update({ - id: '/about', - path: '/about', - getParentRoute: () => rootRoute, -} as any) - -const IndexRoute = IndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRoute, -} as any) - -const AccountsIndexRoute = AccountsIndexImport.update({ - id: '/accounts/', - path: '/accounts/', - getParentRoute: () => rootRoute, -} as any) - -const AccountsAccountIdRoute = AccountsAccountIdImport.update({ - id: '/accounts/$accountId', - path: '/accounts/$accountId', - getParentRoute: () => rootRoute, -} as any) - -// Populate the FileRoutesByPath interface - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/about': { - id: '/about' - path: '/about' - fullPath: '/about' - preLoaderRoute: typeof AboutImport - parentRoute: typeof rootRoute - } - '/budget': { - id: '/budget' - path: '/budget' - fullPath: '/budget' - preLoaderRoute: typeof BudgetImport - parentRoute: typeof rootRoute - } - '/transactions': { - id: '/transactions' - path: '/transactions' - fullPath: '/transactions' - preLoaderRoute: typeof TransactionsImport - parentRoute: typeof rootRoute - } - '/accounts/$accountId': { - id: '/accounts/$accountId' - path: '/accounts/$accountId' - fullPath: '/accounts/$accountId' - preLoaderRoute: typeof AccountsAccountIdImport - parentRoute: typeof rootRoute - } - '/accounts/': { - id: '/accounts/' - path: '/accounts' - fullPath: '/accounts' - preLoaderRoute: typeof AccountsIndexImport - parentRoute: typeof rootRoute - } - } -} - -// Create and export the route tree - -export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/about': typeof AboutRoute - '/budget': typeof BudgetRoute - '/transactions': typeof TransactionsRoute - '/accounts/$accountId': typeof AccountsAccountIdRoute - '/accounts': typeof AccountsIndexRoute -} - -export interface FileRoutesByTo { - '/': typeof IndexRoute - '/about': typeof AboutRoute - '/budget': typeof BudgetRoute - '/transactions': typeof TransactionsRoute - '/accounts/$accountId': typeof AccountsAccountIdRoute - '/accounts': typeof AccountsIndexRoute -} - -export interface FileRoutesById { - __root__: typeof rootRoute - '/': typeof IndexRoute - '/about': typeof AboutRoute - '/budget': typeof BudgetRoute - '/transactions': typeof TransactionsRoute - '/accounts/$accountId': typeof AccountsAccountIdRoute - '/accounts/': typeof AccountsIndexRoute -} - -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/about' - | '/budget' - | '/transactions' - | '/accounts/$accountId' - | '/accounts' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/about' - | '/budget' - | '/transactions' - | '/accounts/$accountId' - | '/accounts' - id: - | '__root__' - | '/' - | '/about' - | '/budget' - | '/transactions' - | '/accounts/$accountId' - | '/accounts/' - fileRoutesById: FileRoutesById -} - -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - AboutRoute: typeof AboutRoute - BudgetRoute: typeof BudgetRoute - TransactionsRoute: typeof TransactionsRoute - AccountsAccountIdRoute: typeof AccountsAccountIdRoute - AccountsIndexRoute: typeof AccountsIndexRoute -} - -const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - AboutRoute: AboutRoute, - BudgetRoute: BudgetRoute, - TransactionsRoute: TransactionsRoute, - AccountsAccountIdRoute: AccountsAccountIdRoute, - AccountsIndexRoute: AccountsIndexRoute, -} - -export const routeTree = rootRoute - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -/* ROUTE_MANIFEST_START -{ - "routes": { - "__root__": { - "filePath": "__root.tsx", - "children": [ - "/", - "/about", - "/budget", - "/transactions", - "/accounts/$accountId", - "/accounts/" - ] - }, - "/": { - "filePath": "index.tsx" - }, - "/about": { - "filePath": "about.tsx" - }, - "/budget": { - "filePath": "budget.tsx" - }, - "/transactions": { - "filePath": "transactions.tsx" - }, - "/accounts/$accountId": { - "filePath": "accounts/$accountId.tsx" - }, - "/accounts/": { - "filePath": "accounts/index.tsx" - } - } -} -ROUTE_MANIFEST_END */ diff --git a/apps/frontend/src/routes/__root.tsx b/apps/frontend/src/routes/__root.tsx deleted file mode 100644 index 16d2c26d..00000000 --- a/apps/frontend/src/routes/__root.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createRootRoute } from '@tanstack/react-router'; - -import { Layout } from '../shared/ui/Layout'; - -export const Route = createRootRoute({ - component: Layout, -}); diff --git a/apps/frontend/src/routes/about.tsx b/apps/frontend/src/routes/about.tsx deleted file mode 100644 index f11d6b84..00000000 --- a/apps/frontend/src/routes/about.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; - -export const Route = createFileRoute('/about')({ - component: About, -}); - -function About() { - return
    Hello from About!
    ; -} diff --git a/apps/frontend/src/routes/accounts/$accountId.tsx b/apps/frontend/src/routes/accounts/$accountId.tsx deleted file mode 100644 index a30632a2..00000000 --- a/apps/frontend/src/routes/accounts/$accountId.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { AccountPage } from 'src/pages/accountPage'; - -export const Route = createFileRoute('/accounts/$accountId')({ - component: AccountPage, -}); diff --git a/apps/frontend/src/routes/accounts/index.tsx b/apps/frontend/src/routes/accounts/index.tsx deleted file mode 100644 index 16e1d689..00000000 --- a/apps/frontend/src/routes/accounts/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { AccountsPage } from 'src/pages/accountsPage'; -export const Route = createFileRoute('/accounts/')({ - component: AccountsPage, -}); diff --git a/apps/frontend/src/routes/budget.tsx b/apps/frontend/src/routes/budget.tsx deleted file mode 100644 index fce1993e..00000000 --- a/apps/frontend/src/routes/budget.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; - -export const Route = createFileRoute('/budget')({ - component: RouteComponent, -}); - -function RouteComponent() { - return
    Hello budget!
    ; -} diff --git a/apps/frontend/src/routes/index.tsx b/apps/frontend/src/routes/index.tsx deleted file mode 100644 index 502300e2..00000000 --- a/apps/frontend/src/routes/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { IndexPage } from 'src/pages/indexPage'; - -export const Route = createFileRoute('/')({ - component: IndexPage, -}); diff --git a/apps/frontend/src/routes/transactions.tsx b/apps/frontend/src/routes/transactions.tsx deleted file mode 100644 index 604fe03d..00000000 --- a/apps/frontend/src/routes/transactions.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; - -export const Route = createFileRoute('/transactions')({ - component: RouteComponent, -}); - -function RouteComponent() { - return
    Hello transactions!
    ; -} diff --git a/apps/frontend/src/shared/api/config.ts b/apps/frontend/src/shared/api/config.ts deleted file mode 100644 index 3b639e06..00000000 --- a/apps/frontend/src/shared/api/config.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { envConfig } from '../config/config'; - -export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - -const path = '/api'; - -const apiConfig = { - baseUrl: (url: string) => `${path}${url}`, - createAbortController: () => new AbortController(), - headers: { - Accept: 'application/json', - Authorization: `Bearer ${envConfig.API_TOKEN}`, - } as Record, - method: 'GET' as HttpMethod, - timeout: 5000, -}; - -type Response = { data: T; ok: true } | { error: string; ok: false }; - -export const fetchWrapper = async ( - path: string, - options: RequestInit = {}, - controller?: AbortController -): Promise> => { - const url = apiConfig.baseUrl(path); - - const abortController = controller ?? apiConfig.createAbortController(); - const timeoutId = setTimeout(() => abortController.abort(), apiConfig.timeout); - - try { - const headers: Record = { - ...apiConfig.headers, - ...(options.headers as Record), - }; - - const response = await fetch(url, { - ...options, - headers, - signal: abortController.signal, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return { data: (await response.json()) as T, ok: true }; - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - throw new Error('Request timed out'); - } - throw error; - } finally { - clearTimeout(timeoutId); - } -}; - -export type BaseActions = { - create: (data: D) => Promise>; - read: () => Promise>; - update: (id: string, data: D) => Promise>; - delete: (id: string) => Promise>; -}; - -export const baseActions = (url: string): BaseActions => ({ - create: async (data) => { - return await fetchWrapper(url, { - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - }); - }, - delete: async (id: string) => { - return await fetchWrapper(`${url}/${id}`, { - method: 'DELETE', - }); - }, - read: async () => { - return await fetchWrapper(url); - }, - update: async (id: string, data) => { - return await fetchWrapper(`${url}/${id}`, { - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, - method: 'PUT', - }); - }, -}); diff --git a/apps/frontend/src/shared/api/index.ts b/apps/frontend/src/shared/api/index.ts deleted file mode 100644 index 5f6f7c28..00000000 --- a/apps/frontend/src/shared/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { baseActions } from './config'; diff --git a/apps/frontend/src/shared/config/config.ts b/apps/frontend/src/shared/config/config.ts deleted file mode 100644 index 27405e17..00000000 --- a/apps/frontend/src/shared/config/config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const envConfig = { - API_TOKEN: import.meta.env.VITE_API_TOKEN, - API_URL: import.meta.env.VITE_API_URL ?? 'http://localhost:3000', - NODE_ENV: import.meta.env.MODE || 'development', -}; diff --git a/apps/frontend/src/shared/lib/modalState/index.ts b/apps/frontend/src/shared/lib/modalState/index.ts deleted file mode 100644 index b96cb3a6..00000000 --- a/apps/frontend/src/shared/lib/modalState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ModalState } from './modalState'; diff --git a/apps/frontend/src/shared/lib/modalState/modalState.ts b/apps/frontend/src/shared/lib/modalState/modalState.ts deleted file mode 100644 index 1a619198..00000000 --- a/apps/frontend/src/shared/lib/modalState/modalState.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { makeAutoObservable } from 'mobx'; - -export class ModalState { - isOpen = false; - - constructor() { - makeAutoObservable(this); - } - - open = () => { - this.isOpen = true; - }; - - close = () => { - this.isOpen = false; - }; -} diff --git a/apps/frontend/src/shared/types/index.ts b/apps/frontend/src/shared/types/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/frontend/src/shared/ui/Button/Button.tsx b/apps/frontend/src/shared/ui/Button/Button.tsx deleted file mode 100644 index fdbb1430..00000000 --- a/apps/frontend/src/shared/ui/Button/Button.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import clsx from 'clsx'; - -interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'neutral'; - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; - ghost?: boolean; - link?: boolean; - outline?: boolean; - soft?: boolean; - wide?: boolean; -} - -const buttonClasses = ({ ghost, link, outline, size, soft, variant, wide }: ButtonProps) => { - const baseClass = 'btn'; - const variantClass = variant ? `btn-${variant}` : 'btn-primary'; - const sizeClass = size ? `btn-${size}` : 'btn-md'; - const ghostClass = ghost ? 'btn-ghost' : ''; - const linkClass = link ? 'btn-link' : ''; - const outlineClass = outline ? 'btn-outline' : ''; - const softClass = soft ? 'btn-soft' : ''; - const wideClass = wide ? 'btn-wide' : ''; - - return [baseClass, variantClass, sizeClass, ghostClass, linkClass, outlineClass, softClass, wideClass] - .join(' ') - .trim(); -}; - -export const Button: React.FC = ({ children, className, size = 'md', variant = 'primary', ...props }) => { - const buttonClass = buttonClasses({ ...props, size, variant }); - - return ( - - ); -}; diff --git a/apps/frontend/src/shared/ui/Button/index.ts b/apps/frontend/src/shared/ui/Button/index.ts deleted file mode 100644 index fe9c53c5..00000000 --- a/apps/frontend/src/shared/ui/Button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Button } from './Button'; diff --git a/apps/frontend/src/shared/ui/CloseButton/CloseButton.tsx b/apps/frontend/src/shared/ui/CloseButton/CloseButton.tsx deleted file mode 100644 index 607da7e8..00000000 --- a/apps/frontend/src/shared/ui/CloseButton/CloseButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { X } from 'lucide-react'; - -type CloseButtonProps = { - onClick?: () => void; -}; - -export const CloseButton = ({ onClick }: CloseButtonProps) => { - return ( - - ); -}; diff --git a/apps/frontend/src/shared/ui/CloseButton/index.ts b/apps/frontend/src/shared/ui/CloseButton/index.ts deleted file mode 100644 index 5b054621..00000000 --- a/apps/frontend/src/shared/ui/CloseButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CloseButton } from './CloseButton'; diff --git a/apps/frontend/src/shared/ui/Layout/Footer/Footer.tsx b/apps/frontend/src/shared/ui/Layout/Footer/Footer.tsx deleted file mode 100644 index 056b43cc..00000000 --- a/apps/frontend/src/shared/ui/Layout/Footer/Footer.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const Footer = () => { - return ( -
    -

    © 2025 FinTracker

    -
    - ); -}; diff --git a/apps/frontend/src/shared/ui/Layout/Footer/index.ts b/apps/frontend/src/shared/ui/Layout/Footer/index.ts deleted file mode 100644 index 65e2506f..00000000 --- a/apps/frontend/src/shared/ui/Layout/Footer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Footer } from './Footer'; diff --git a/apps/frontend/src/shared/ui/Layout/Header/Header.tsx b/apps/frontend/src/shared/ui/Layout/Header/Header.tsx deleted file mode 100644 index e2329702..00000000 --- a/apps/frontend/src/shared/ui/Layout/Header/Header.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Link } from '@tanstack/react-router'; - -const links: { to: string; label: string }[] = [ - { label: 'Home', to: '/' }, - { label: 'About', to: '/about' }, -]; - -export const Header = () => { - return ( -
    -

    ФинТрекер

    -
    - {links.map((link) => ( - - {link.label} - - ))} -
    -
    - ); -}; diff --git a/apps/frontend/src/shared/ui/Layout/Header/index.ts b/apps/frontend/src/shared/ui/Layout/Header/index.ts deleted file mode 100644 index 29429dc9..00000000 --- a/apps/frontend/src/shared/ui/Layout/Header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Header } from './Header'; diff --git a/apps/frontend/src/shared/ui/Layout/Layout.tsx b/apps/frontend/src/shared/ui/Layout/Layout.tsx deleted file mode 100644 index e1a75d46..00000000 --- a/apps/frontend/src/shared/ui/Layout/Layout.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from 'react'; - -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; -import { currencyState } from 'src/entities/currencies'; -import { ConfirmDialog } from 'src/features/ConfirmDialog'; - -import { Footer } from './Footer'; -import { Header } from './Header'; -import { Main } from './Main'; -import { Sidebar } from './Sidebar/Sidebar'; - -export const Layout = () => { - useEffect(() => { - void currencyState.getAll(); - }, []); - - return ( -
    -
    - -
    -
    - -
    - ); -}; - -export const Layout2 = () => { - return ( -
    -
    -
    - -
    -
    -
    - -
    - ); -}; diff --git a/apps/frontend/src/shared/ui/Layout/Main/Main.tsx b/apps/frontend/src/shared/ui/Layout/Main/Main.tsx deleted file mode 100644 index c2026763..00000000 --- a/apps/frontend/src/shared/ui/Layout/Main/Main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Outlet } from '@tanstack/react-router'; - -export const Main = () => { - return ( -
    -
    - -
    -
    - ); -}; diff --git a/apps/frontend/src/shared/ui/Layout/Main/index.ts b/apps/frontend/src/shared/ui/Layout/Main/index.ts deleted file mode 100644 index a1a31000..00000000 --- a/apps/frontend/src/shared/ui/Layout/Main/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Main } from './Main'; diff --git a/apps/frontend/src/shared/ui/Layout/Sidebar/Sidebar.tsx b/apps/frontend/src/shared/ui/Layout/Sidebar/Sidebar.tsx deleted file mode 100644 index a401ac99..00000000 --- a/apps/frontend/src/shared/ui/Layout/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { accountsState } from 'src/entities/accounts/model/accountsState'; -import { AccountsList } from 'src/entities/accounts/ui/AccountsList'; -import { ManageAccountModal } from 'src/features/ManageAccount/ui'; - -import { Button } from '../../Button'; - -export const Sidebar = () => { - return ( -
    - - - -
    - ); -}; diff --git a/apps/frontend/src/shared/ui/Layout/Sidebar/index.ts b/apps/frontend/src/shared/ui/Layout/Sidebar/index.ts deleted file mode 100644 index d440597f..00000000 --- a/apps/frontend/src/shared/ui/Layout/Sidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Sidebar } from './Sidebar'; diff --git a/apps/frontend/src/shared/ui/Layout/index.ts b/apps/frontend/src/shared/ui/Layout/index.ts deleted file mode 100644 index 9fc685e2..00000000 --- a/apps/frontend/src/shared/ui/Layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Layout } from './Layout'; diff --git a/apps/frontend/src/shared/ui/Modal/Modal.tsx b/apps/frontend/src/shared/ui/Modal/Modal.tsx deleted file mode 100644 index 6a0be380..00000000 --- a/apps/frontend/src/shared/ui/Modal/Modal.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { createContext, useContext, type ReactNode } from 'react'; - -import clsx from 'clsx'; -import { CloseButton } from 'src/shared/ui/CloseButton'; - -type MaxWidth = - | 'max-w-sm' - | 'max-w-md' - | 'max-w-lg' - | 'max-w-xl' - | 'max-w-2xl' - | 'max-w-3xl' - | 'max-w-4xl' - | 'max-w-5xl'; - -type ModalContext = { - onClose: () => void | null; -}; - -const ModalContext = createContext(null); - -const useModalContext = () => { - const context = useContext(ModalContext); - - if (!context) { - throw new Error('useModalContext must be used within a ModalProvider'); - } - - return context; -}; - -const ModalProvider = ({ children, onClose }: { children: ReactNode; onClose: () => void | null }) => { - return {children}; -}; - -type ModalHeaderProps = { - children?: ReactNode; - title?: string; - withCloseButton?: boolean; - className?: string; -}; - -const ModalHeader = (props: ModalHeaderProps) => { - const { onClose } = useModalContext(); - const { children, className, title, withCloseButton } = props; - - if (!(title || children)) { - throw new Error('ModalHeader requires either a title or children prop'); - } - - return ( -
    - {title &&

    {title}

    } - {children &&
    {children}
    } - {withCloseButton && } -
    - ); -}; - -type ModalFooterProps = { - children: ReactNode; - className?: string; -}; - -const ModalFooter = (props: ModalFooterProps) => { - const { children, className } = props; - return
    {children}
    ; -}; - -type ModalProps = { - children: ReactNode; - isOpen?: boolean; - onClose: () => void; - zIndex?: 'high' | 'low'; - maxWidth?: MaxWidth; - className?: string; -}; - -const Modal = (props: ModalProps) => { - const { children, className, isOpen, maxWidth = 'max-w-lg', onClose, zIndex } = props; - - if (!isOpen) { - return null; - } - - const getModalClass = () => { - const baseClass = 'modal modal-open'; - if (zIndex === 'high') return `${baseClass} z-[1000]`; - if (zIndex === 'low') return `${baseClass} z-[50]`; - return baseClass; - }; - - // const getBackdropClass = () => { - // const baseClass = 'modal-backdrop'; - // if (zIndex === 'high') return `${baseClass} z-[999]`; - // if (zIndex === 'low') return `${baseClass} z-[49]`; - // return baseClass; - // }; - - return ( - -
    -
    {children}
    - {/*
    -
    - ); -}; - -const ModalBody = ({ children }: { children: ReactNode }) => { - return
    {children}
    ; -}; - -Modal.Header = ModalHeader; -Modal.Body = ModalBody; -Modal.Footer = ModalFooter; -Modal.displayName = 'Modal'; -ModalHeader.displayName = 'ModalHeader'; -ModalFooter.displayName = 'ModalFooter'; - -export { Modal, ModalHeader, ModalBody, ModalFooter }; diff --git a/apps/frontend/src/shared/ui/Modal/index.ts b/apps/frontend/src/shared/ui/Modal/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/frontend/src/shared/ui/Modal/modal.css b/apps/frontend/src/shared/ui/Modal/modal.css deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/frontend/src/shared/ui/Navbar/Navbar.tsx b/apps/frontend/src/shared/ui/Navbar/Navbar.tsx deleted file mode 100644 index 61a1de5a..00000000 --- a/apps/frontend/src/shared/ui/Navbar/Navbar.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Link } from '@tanstack/react-router'; - -import { sidebarItems } from './navbarConfig'; - -export const Navbar = () => { - return ( - - ); -}; diff --git a/apps/frontend/src/shared/ui/Navbar/index.ts b/apps/frontend/src/shared/ui/Navbar/index.ts deleted file mode 100644 index ec1cfd76..00000000 --- a/apps/frontend/src/shared/ui/Navbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Navbar } from './Navbar'; diff --git a/apps/frontend/src/shared/ui/Navbar/navbarConfig.ts b/apps/frontend/src/shared/ui/Navbar/navbarConfig.ts deleted file mode 100644 index 93ba8896..00000000 --- a/apps/frontend/src/shared/ui/Navbar/navbarConfig.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BarChart3, CreditCard, DollarSign, Home, LineChart, List, PieChart, Settings } from 'lucide-react'; - -export const sidebarItems: { - title: string; - items: { to: string; icon: React.ElementType; label: string }[]; -}[] = [ - { - items: [ - { icon: Home, label: 'Обзор', to: '/' }, - { icon: List, label: 'Транзакции', to: '/transactions' }, - { icon: CreditCard, label: 'Счета', to: '/accounts' }, - { icon: DollarSign, label: 'Бюджет', to: '/budget' }, - ], - title: 'Основное', - }, - { - items: [ - { icon: BarChart3, label: 'Отчеты', to: '#' }, - { icon: LineChart, label: 'Тренды', to: '#' }, - { icon: PieChart, label: 'Категории', to: '#' }, - ], - title: 'Аналитика', - }, - { - items: [{ icon: Settings, label: 'Настройки', to: '#' }], - title: 'Настройки', - }, -]; diff --git a/apps/frontend/src/widgets/Dashboard/Dashboard.tsx b/apps/frontend/src/widgets/Dashboard/Dashboard.tsx deleted file mode 100644 index 9a6d6ac2..00000000 --- a/apps/frontend/src/widgets/Dashboard/Dashboard.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const Dashboard = () => { - return ( -
    -

    Dashboard

    -

    Welcome to the dashboard!

    -
    - ); -}; diff --git a/apps/frontend/src/widgets/Dashboard/index.ts b/apps/frontend/src/widgets/Dashboard/index.ts deleted file mode 100644 index 41a54b59..00000000 --- a/apps/frontend/src/widgets/Dashboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Dashboard } from './Dashboard'; diff --git a/apps/frontend/tsconfig.app.tsbuildinfo b/apps/frontend/tsconfig.app.tsbuildinfo index 23e92903..0c4a1ae8 100644 --- a/apps/frontend/tsconfig.app.tsbuildinfo +++ b/apps/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/client.d.ts","../../node_modules/.pnpm/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.d.ts","../../node_modules/.pnpm/tiny-warning@1.0.3/node_modules/tiny-warning/src/index.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/types.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/store.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/derived.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/effect.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/scheduler.d.ts","../../node_modules/.pnpm/@tanstack+store@0.7.0/node_modules/@tanstack/store/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/fileroute.d.ts","../../node_modules/.pnpm/@tanstack+history@1.115.0/node_modules/@tanstack/history/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/utils.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/location.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/link.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/routeinfo.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/not-found.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/matches.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/root.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/routerprovider.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/route.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/validators.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/searchparams.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/redirect.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/manifest.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/serializer.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/router.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/defer.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/path.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/qss.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/searchmiddleware.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/structuralsharing.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useroutecontext.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/usesearch.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useparams.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/usenavigate.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useloaderdeps.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/useloaderdata.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/scroll-restoration.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/typeprimitives.d.ts","../../node_modules/.pnpm/@tanstack+router-core@1.120.5/node_modules/@tanstack/router-core/dist/esm/index.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/serializer.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/awaited.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/structuralsharing.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useloaderdata.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usematch.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useloaderdeps.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useparams.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usesearch.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useroutecontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/typeprimitives.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/link.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/route.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/catchboundary.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/clientonly.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/fileroute.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/history.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/lazyroutecomponent.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/matches.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/matchcontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/match.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/router.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/routerprovider.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scrollrestoration.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/useblocker.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usenavigate.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/routercontext.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/userouter.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/userouterstate.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/uselocation.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/usecangoback.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/utils.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/not-found.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scriptonce.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/asset.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/headcontent.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/scripts.d.ts","../../node_modules/.pnpm/@tanstack+react-router@1.120.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@tanstack/react-router/dist/esm/index.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/scheduler.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/jsx.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/component.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/flow.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/suspense.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/hydration.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/render/index.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/signal.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/observable.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/reactive/array.d.ts","../../node_modules/.pnpm/solid-js@1.9.7/node_modules/solid-js/types/index.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/tanstackrouterdevtools.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/tanstackrouterdevtoolspanel.d.ts","../../node_modules/.pnpm/@tanstack+react-router-devtools@1.120.5_@tanstack+react-router@1.120.5_react-dom@19.1.0_cf56cc0117e73b14ba68cd3aaf107914/node_modules/@tanstack/react-router-devtools/dist/esm/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/typealiases.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/util.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/zoderror.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/locales/en.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/errors.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/parseutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/enumutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/errorutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/partialutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/standard-schema.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/types.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/external.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/index.d.ts","../../packages/shared/src/constants/pagination.ts","../../packages/shared/src/constants/transactions.ts","../../packages/shared/src/validation/basevalidations.ts","../../packages/shared/src/types/types.ts","../../packages/shared/src/types/account.ts","../../packages/shared/src/types/user.ts","../../packages/shared/src/constants/accounts.ts","../../packages/shared/src/constants/currencies.ts","../../packages/shared/src/constants/operations.ts","../../packages/shared/src/constants/index.ts","../../packages/shared/src/validation/accounts.ts","../../packages/shared/src/validation/transactions.ts","../../packages/shared/src/validation/users.ts","../../packages/shared/src/validation/auth.ts","../../packages/shared/src/validation/index.ts","../../packages/shared/src/types/auth.ts","../../packages/shared/src/types/error-contract.ts","../../packages/shared/src/types/pagination.ts","../../packages/shared/src/types/transaction.ts","../../packages/shared/src/types/index.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/global.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/errors.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/decorator_fills.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/decorators.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/atom.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/comparer.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/modifiers.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/overrideannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/actionannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/flowannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/computedannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/autoannotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/generic-abort-signal.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/observable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/computed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/action.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablevalue.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/computedvalue.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/derivation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/globalstate.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/observable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/reaction.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableobject.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablearray.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/core/spy.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/action.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/autorun.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/become-observed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/configure.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/extendobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/extras.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/flow.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/intercept-read.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/intercept.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/iscomputed.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/isobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/object-api.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/observe.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/tojs.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/trace.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/transaction.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/when.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/dynamicobject.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/intercept-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/listen-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/makeobservable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observablemap.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/observableset.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/legacyobservablearray.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/types/type-utils.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/eq.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/utils/iterable.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/api/annotation.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/internal.d.ts","../../node_modules/.pnpm/mobx@6.13.7/node_modules/mobx/dist/mobx.d.ts","../../packages/shared/src/routes/index.ts","./src/shared/config/config.ts","./src/shared/api/config.ts","./src/shared/api/index.ts","./src/entities/currencies/api/currenciesactions.ts","./src/entities/currencies/model/currencystate.ts","./src/entities/currencies/index.ts","../../node_modules/.pnpm/lucide-react@0.507.0_react@19.1.0/node_modules/lucide-react/dist/lucide-react.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/assertenvironment.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/universalfinalizationregistry.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/observerfinalizationregistry.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/staticrendering.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/observer.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/observercomponent.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/uselocalobservable.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/uselocalstore.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/useasobservablesource.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/utils/observerbatching.d.ts","../../node_modules/.pnpm/mobx-react-lite@4.1.0_mobx@6.13.7_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/mobx-react-lite/dist/index.d.ts","../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/clsx.d.mts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/closebutton/closebutton.tsx","./src/shared/ui/closebutton/index.ts","./src/shared/ui/modal/modal.tsx","./src/features/confirmdialog/model/confirmdialogstate.ts","./src/features/confirmdialog/ui/confirmdialog.tsx","./src/features/confirmdialog/index.ts","./src/shared/ui/layout/footer/footer.tsx","./src/shared/ui/layout/footer/index.ts","./src/shared/ui/layout/header/header.tsx","./src/shared/ui/layout/header/index.ts","./src/shared/ui/layout/main/main.tsx","./src/shared/ui/layout/main/index.ts","./src/shared/lib/modalstate/modalstate.ts","./src/shared/lib/modalstate/index.ts","./src/entities/accounts/api/accountactions.ts","./src/entities/accounts/model/accountsstate.ts","./src/entities/accounts/ui/accountslist/accountslist.tsx","./src/entities/accounts/ui/accountslist/index.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/constants.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/createsubject.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/events.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/common.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/eager.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/path/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/fieldarray.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/resolvers.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/form.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/utils.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/fields.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/errors.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/validator.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/controller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/types/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/controller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/form.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/appenderrors.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/createformcontrol.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/logic/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usecontroller.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usefieldarray.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useform.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useformcontext.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/useformstate.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/usewatch.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/get.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/set.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/utils/index.d.ts","../../node_modules/.pnpm/react-hook-form@7.56.4_react@19.1.0/node_modules/react-hook-form/dist/index.d.ts","../../node_modules/.pnpm/@hookform+resolvers@5.0.1_react-hook-form@7.56.4_react@19.1.0_/node_modules/@hookform/resolvers/zod/dist/zod.d.ts","../../node_modules/.pnpm/@hookform+resolvers@5.0.1_react-hook-form@7.56.4_react@19.1.0_/node_modules/@hookform/resolvers/zod/dist/index.d.ts","./src/features/manageaccount/ui/manageaccountform.tsx","./src/features/manageaccount/ui/manageaccountmodal.tsx","./src/features/manageaccount/ui/index.ts","./src/shared/ui/layout/sidebar/sidebar.tsx","./src/shared/ui/layout/layout.tsx","./src/shared/ui/layout/index.ts","./src/routes/__root.tsx","./src/routes/transactions.tsx","./src/routes/budget.tsx","./src/routes/about.tsx","./src/pages/indexpage/indexpage.tsx","./src/pages/indexpage/index.ts","./src/routes/index.tsx","./src/pages/accountspage/accountspage.tsx","./src/pages/accountspage/index.ts","./src/routes/accounts/index.tsx","./src/pages/accountpage/accountpage.tsx","./src/pages/accountpage/index.ts","./src/routes/accounts/$accountid.tsx","./src/routetree.gen.ts","./src/main.tsx","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/hmrpayload.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/customevent.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/hot.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/importglob.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/importmeta.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/client.d.ts","./src/vite-env.d.ts","./src/entities/transactions/api/transactionsactions.ts","./src/entities/transactions/model/transactionsstate.ts","./src/shared/types/index.ts","./src/shared/ui/layout/sidebar/index.ts","./src/shared/ui/modal/index.ts","./src/shared/ui/navbar/navbarconfig.ts","./src/shared/ui/navbar/navbar.tsx","./src/shared/ui/navbar/index.ts","./src/widgets/dashboard/dashboard.tsx","./src/widgets/dashboard/index.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/types.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/index.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/index.d.ts","../../packages/shared/src/libs/formatters.ts","../../packages/shared/src/libs/index.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/index.d.ts"],"fileIdsList":[[51,176,234,237,355,397],[51,176,233,269,270,355,397],[50,51,128,176,241,252,255,271,325,326,355,397],[51,272,355,397],[51,239,355,397],[51,176,233,238,355,397],[51,355,397],[51,259,260,355,397],[51,233,355,397],[51,241,252,255,258,259,355,397],[51,307,355,397],[51,166,171,176,240,252,271,303,305,355,397],[51,252,255,258,261,271,306,355,397],[50,51,52,128,325,326,332,355,397],[51,128,325,326,355,397],[51,322,355,397],[51,319,355,397],[51,316,355,397],[51,128,311,325,326,355,397],[51,128,323,325,326,355,397],[51,128,320,325,326,355,397],[51,128,317,325,326,355,397],[51,128,312,313,314,315,318,321,324,326,355,397],[51,235,355,397],[51,236,355,397],[51,268,355,397],[50,51,253,355,397],[51,254,355,397],[51,241,355,397],[51,256,355,397],[51,262,355,397],[51,264,355,397],[51,310,355,397],[50,51,142,240,261,263,265,267,309,355,397],[51,266,355,397],[51,309,355,397],[51,255,271,273,308,355,397],[50,51,253,257,355,397],[51,340,355,397],[51,128,325,326,339,355,397],[332,355,397],[51,342,355,397],[304,355,397],[156,303,355,397],[355,397],[140,141,355,397],[50,128,139,325,326,355,397],[50,128,325,326,355,397],[91,92,103,109,112,355,397],[50,51,91,92,103,109,112,355,397],[50,51,103,355,397],[50,51,355,397],[91,92,95,96,97,98,99,100,103,109,112,355,397],[51,91,92,103,109,112,355,397],[62,355,397],[53,54,62,91,92,93,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,355,397],[103,355,397],[50,91,92,101,103,109,112,355,397],[50,355,397],[50,51,91,92,94,103,109,112,355,397],[50,91,92,95,96,97,98,99,100,102,103,109,112,355,397],[62,91,92,103,107,109,112,355,397],[50,91,92,103,109,112,355,397],[50,91,103,109,112,355,397],[91,92,98,99,102,103,109,112,355,397],[50,62,91,92,103,107,109,112,355,397],[91,92,94,103,109,112,355,397],[77,355,397],[71,72,355,397],[61,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,355,397],[62,63,64,66,77,107,355,397],[62,72,107,355,397],[63,66,71,77,355,397],[66,77,355,397],[70,71,355,397],[63,65,77,355,397],[61,63,64,65,66,67,68,69,70,72,77,355,397],[61,63,65,71,77,355,397],[60,62,63,64,65,66,67,68,70,71,72,73,74,75,76,78,107,355,397],[64,65,66,77,355,397],[63,64,77,355,397],[63,65,71,355,397],[72,355,397],[63,355,397],[63,65,66,74,77,84,85,355,397],[63,66,77,355,397],[65,77,355,397],[71,355,397],[55,56,355,397],[57,355,397],[55,56,57,58,59,355,397],[56,57,355,397],[55,355,397],[355,394,397],[355,396,397],[397],[355,397,402,432],[355,397,398,403,409,410,417,429,440],[355,397,398,399,409,417],[350,351,352,355,397],[355,397,400,441],[355,397,401,402,410,418],[355,397,402,429,437],[355,397,403,405,409,417],[355,396,397,404],[355,397,405,406],[355,397,409],[355,397,407,409],[355,396,397,409],[355,397,409,410,411,429,440],[355,397,409,410,411,424,429,432],[355,392,397,445],[355,392,397,405,409,412,417,429,440],[355,397,409,410,412,413,417,429,437,440],[355,397,412,414,429,437,440],[353,354,355,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446],[355,397,409,415],[355,397,416,440],[355,397,405,409,417,429],[355,397,418],[355,397,419],[355,396,397,420],[355,394,395,396,397,398,399,400,401,402,403,404,405,406,407,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446],[355,397,422],[355,397,423],[355,397,409,424,425],[355,397,424,426,441,443],[355,397,409,429,430,432],[355,397,431,432],[355,397,429,430],[355,397,432],[355,397,433],[355,394,397,429],[355,397,409,435,436],[355,397,435,436],[355,397,402,417,429,437],[355,397,438],[355,397,417,439],[355,397,412,423,440],[355,397,402,441],[355,397,429,442],[355,397,416,443],[355,397,444],[355,397,402,409,411,420,429,440,443,445],[355,397,429,446],[48,49,355,397],[345,355,397],[344,355,397],[242,244,245,246,247,248,249,250,251,355,397],[233,355,397],[233,243,355,397],[180,232,355,397],[232,355,397],[196,232,355,397],[195,196,201,202,232,355,397],[177,178,179,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,355,397],[50,288,355,397],[288,289,290,293,294,295,296,297,298,299,302,355,397],[288,355,397],[291,292,355,397],[50,286,288,355,397],[283,284,286,355,397],[279,282,284,286,355,397],[283,286,355,397],[50,274,275,276,279,280,281,283,284,285,286,355,397],[276,279,280,281,282,283,284,285,286,287,355,397],[283,355,397],[277,283,284,355,397],[277,278,355,397],[282,284,285,355,397],[282,355,397],[274,279,284,285,355,397],[300,301,355,397],[129,130,135,136,137,138,355,397],[49,355,397],[136,355,397],[129,130,135,355,397],[130,355,397],[130,136,355,397],[131,132,133,134,355,397],[355,364,368,397,440],[355,364,397,429,440],[355,359,397],[355,361,364,397,437,440],[355,397,417,437],[355,397,447],[355,359,397,447],[355,361,364,397,417,440],[355,356,357,360,363,397,409,429,440],[355,364,371,397],[355,356,362,397],[355,364,385,386,397],[355,360,364,397,432,440,447],[355,385,397,447],[355,358,359,397,447],[355,364,397],[355,358,359,360,361,362,363,364,365,366,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,386,387,388,389,390,391,397],[355,364,379,397],[355,364,371,372,397],[355,362,364,372,373,397],[355,363,397],[355,356,359,364,397],[355,364,368,372,373,397],[355,368,397],[355,362,364,367,397,440],[355,356,361,364,371,397],[355,397,429],[355,359,364,385,397,445,447],[331,355,397],[327,355,397],[328,355,397],[329,330,355,397],[155,355,397],[145,146,355,397],[143,144,145,147,148,153,355,397],[144,145,355,397],[154,355,397],[145,355,397],[143,144,145,148,149,150,151,152,355,397],[143,144,155,355,397],[176,355,397],[157,158,163,164,165,355,397],[157,355,397],[346,355,397],[347,355,397],[160,355,397],[156,171,355,397],[160,161,162,172,173,174,175,355,397],[156,159,160,174,355,397],[156,159,355,397],[156,159,166,355,397],[156,355,397],[156,157,158,355,397],[159,167,168,169,170,355,397],[156,158,159,355,397]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"742d4b7b02ffc3ba3c4258a3d196457da2b3fec0125872fd0776c50302a11b9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"03566a51ebc848dec449a4ed69518e9f20caa6ac123fa32676aaaabe64adae8e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"88e9caa9c5d2ba629240b5913842e7c57c5c0315383b8dc9d436ef2b60f1c391","impliedFormat":1},{"version":"b8903e4347ba78328892f8c30b45a8a0071e06c7bae0b0a621035fbf36f75bcb","impliedFormat":99},{"version":"5771ec76e11a5e8303d8945e8050daee051436150d5285dc818c8df3239f45bb","impliedFormat":1},{"version":"7f9ef84dcfff2dbd0c8ed9f4ea18216619527f5b79df115a430114ea5a77c3f6","impliedFormat":99},{"version":"368514ed24d7fa3dbbd3360ee9aac4a1e629969268276e92b86c0f672f3fbbb3","impliedFormat":99},{"version":"c2845147e33e886027684d887ec5ddfccf5bc16bc5fd2ec0f244883d5cee4318","impliedFormat":99},{"version":"ead2432b3eb851e40625006ccba0cb39325fa7f72fdbaedd19825bff05722f8e","impliedFormat":99},{"version":"a1ae11570949b69e4a4e0ee1adcd109f3258df90ec3c5d4345d276e629d38366","impliedFormat":99},{"version":"c7c33f7321ee10856267aaedfd6b61cf249e608a6a37ea7204ec84d48f9a1e4b","impliedFormat":99},{"version":"648db4b8b239ed5b5e8c93618bbe4384d69d659ddf040ad200ee4915aee2157e","impliedFormat":99},{"version":"b39c758625e62b1cf23c7cc38bbe561ba53463237a44291486179bc8c4ec138a","impliedFormat":99},{"version":"33686400541314fa6a0757685b354778628768e2b0ef75310027b2d805cbb3aa","impliedFormat":99},{"version":"c630d607581ce676da4f470e1439fa7127bd763a3608a811122a6a1b33264a61","impliedFormat":99},{"version":"d33c779e38298df4fa486c3190beb16194ca42a41947abcb4fc491e6f9944a8c","impliedFormat":99},{"version":"bdfa3b05b56953affb355a81976e6bfb35961ce63b746d6382ce3412c8e0be10","impliedFormat":99},{"version":"169ef5a18d968eefdcdbad749ded93d1127eb40dd78ce0a0e2ecd9d42eea1a1a","impliedFormat":99},{"version":"55a3b2901bf0b3e5c482ea6ae5e34469d9e7242a30a7bf4783c9ffce3cfc8aa3","impliedFormat":99},{"version":"8a30efabf741de93c2884ffdc23f16602042449e28729eb9874953b3ea8dc705","impliedFormat":99},{"version":"ae2845848a8a72c5e211230fc6bdb1b001056836c4ccbed881441ecece9ab23c","impliedFormat":99},{"version":"dc6b45c8b0599be8ca11a2e7e96745072b9f12850fb0eeec7a70486dc9f99712","impliedFormat":99},{"version":"28c6caf88a757b504f8a36901e4cb7571098f92eaf971aaf8ff77101e47a72ac","impliedFormat":99},{"version":"35b4c072ee1aaddc8352e5a40f6d5c7dc15865ff0681cf312fef258362cedfe4","impliedFormat":99},{"version":"88879ddcc4dd4d80d7580d09db58d64e01bdf93def80c4a3eeeb8886f7c3500c","impliedFormat":99},{"version":"0923187df6538b37c811a365ef1d7efa9f1b23c0116fe9255841548311e7b18d","impliedFormat":99},{"version":"5aef5866733347f6fbdd9391e8c2a3c58002472b57c7188912d995e90274a24c","impliedFormat":99},{"version":"19c9ef1751b20765eeb092adbc9f77bf0f5674ef6083bace3598859e629611d8","affectsGlobalScope":true,"impliedFormat":99},{"version":"dcd742a716cc29789bc177f1b3fbf3c8bc2c164b31104635e3e9f84881bf4c2e","impliedFormat":99},{"version":"22568395a672835b8f58b51097b55d914e0939b71b9a8a54754865b590e9ac29","impliedFormat":99},{"version":"b6783f68397f6edf1c6bed6c2d7c0d89c6d443df1f3a6453ced1d4e184e7014c","impliedFormat":99},{"version":"2ee61832845cf9fd22b581a4cf2bb53794fe4043bd1f1c130a5568e6e3e21368","impliedFormat":99},{"version":"aed243366f9c480cd45623a8f2266c2c8868773cdb13db8f46acb2173cd407f1","impliedFormat":99},{"version":"9c372493f8a4e1c6276f1ba3e6521122b65d9de25d34dc8e43effc97fd64be54","impliedFormat":99},{"version":"d90b155a976d4312c36a3dd442bcd52e9326e599c523faae4cfb8942468f5298","impliedFormat":99},{"version":"13876cda8b941c4bff97ae9ddf89f2a04c8e4ff59bf9141f72f892815bf30244","impliedFormat":99},{"version":"f4cc088c0853a0bc7b5c65fcb53422898cb4a412ba46ed6c62716d465a351922","impliedFormat":99},{"version":"65657868876a64f14656e233a5f2e73d354fb824794f934e9543aa7c3aebf51d","impliedFormat":99},{"version":"b220e3999b3cbae6d40e997f4227bc4006ab04f511e5d4636dfd63badeffd025","impliedFormat":99},{"version":"3dab370414f0c44d2e2e0acfd10fff6a57de0ad770c6c26456f31cbf21204f16","impliedFormat":99},{"version":"9b02e7d7ab9800f9c65547f697875e45ea533fd31da0ce06e52798032d3bf2c2","impliedFormat":99},{"version":"65735c7848f6f0cebe410726ec8f647859df9667ccad5972674646f79527d684","impliedFormat":99},{"version":"fb0b313c8baca67889b6048356af082e02de1df625f0e9f8962f397ea3ed560e","impliedFormat":99},{"version":"57eb2b3764a445ce26575a813ca9b8999c7239f70ae435140ab662be599a60f4","impliedFormat":99},{"version":"91a93ec2ea237ab400834525faed8b2214852ee6b080dfc14de4b52d765248b7","impliedFormat":99},{"version":"32485636a4ae3491437b850a2f8ed523c63d59da3ec48375601458c4e59819e4","impliedFormat":99},{"version":"a61b89063f853e9a1e16a029b67588dde050af09a3c3427097f21e9bd5bdd158","impliedFormat":99},{"version":"048375d7ff42f93ed5a29318ac1642a83c02ae0c2fd658f345d1c46e0c54a67f","impliedFormat":99},{"version":"f7a3ce9cc643a4b47429ec47de60555f6ae6877523a2d3f43dd9ef7b0e7556cb","impliedFormat":99},{"version":"eb2e6827dc100235cc062144f983d5851b13a0531a8965a42e71bd5218355141","impliedFormat":99},{"version":"d2e4f966279a90171e6351b1bf6706d14054a4e5bcc8d46948fed172cfe11371","impliedFormat":99},{"version":"32515c5c844173bf500057ab01e2c246cf914d92cd10d0fd4767b0f9591627ae","impliedFormat":99},{"version":"5271013c661bf776d575ce29b8a18565c4674434c70af21f542dffd15d29581f","impliedFormat":99},{"version":"e14172688f53d888134dd0d2c90fd9f0205179db8ab99430d5b1309e50e0cdb8","impliedFormat":99},{"version":"6946f7e29e3bc5cb0da6df5b664d628fb47593f4dc6bdfb141ad5e7d7a4e777b","impliedFormat":99},{"version":"396fb90417ba85a0b114a2b8f7887d68a208293ab7f29b08ae7b7010d2f4de47","impliedFormat":99},{"version":"b9abf61560dc97324a08cdeecbed287440b70c9821d025d483f8ce0ff6907f98","impliedFormat":99},{"version":"d59755c3b9f1ce4bc1379ba54846eaf464b9f4db9026e3cbcd25603cae7562e2","impliedFormat":99},{"version":"a610f9cf754bf9513b44ecb4c50d79928d2d8289b7ef31c67a89b105b3492702","impliedFormat":99},{"version":"7d318d2247741b3cc384060c60e77295ffa7210b1df48eb3e2d1533017af3182","impliedFormat":99},{"version":"f11d572e430c0ea5e957ec85608629c7433b0ad6e0b1450f3e9d91667625fb58","impliedFormat":99},{"version":"d41befd3d4185f93a5ccc1bb099891415b7157d0ede46ae88a20210d4e772bbc","impliedFormat":99},{"version":"c8bbb9ce1c891efe0e45fd16fb5c4f0c06e9ad60ec1e025bb0b161ef71f04371","impliedFormat":99},{"version":"e9124172677261d101c54c55f9025727c0eed6e5c071db507f977b9cfece01ac","impliedFormat":99},{"version":"556a85bb9982f7f12101fbb9a96262f1d7a4ad6bd5cb50db62153438ccf373b5","impliedFormat":99},{"version":"e5af5fb67c619f0f050cea978ed642da92bbbad4d24b9b0373432e7ed3199431","impliedFormat":99},{"version":"2163deee5fbd66077b4fbd4d9cf84c8f06918113eff425a62dc257b051eccd02","impliedFormat":99},{"version":"e8ef79b944df8cbaeed8bcf01062709c91853ea06474571748f65e3a03439656","affectsGlobalScope":true,"impliedFormat":99},{"version":"16db9485a97fb736ec9098a6d1277e7b95fc489c8e8608d83824728f47219d94","impliedFormat":99},{"version":"9e1ad280ec936e7789a3901f29b6ec2b68578ca030ef7c4b51e970d6308aa6e2","impliedFormat":99},{"version":"81f50c2e9a72a15dcfd77a3aff3b676bbf2f270265c3e5ab8d64ba07575417ee","impliedFormat":99},{"version":"038780a3ac1630c2398d24aa98283d23216feebc22a265caa5f747d85b9d0b68","impliedFormat":99},{"version":"3562836bbc8f85daa391c5bac66cc6c85be806b7d1a0bf25bc6897481e555868","impliedFormat":99},{"version":"5dda66d705930bbea2e79e0f0a5b74c5642481c73487e9f48b6e6e2a7535321c","impliedFormat":99},{"version":"fcb569cff31c9ffc80018fe31585a7733d6297977b6d5303a7b0c9c87856e41b","impliedFormat":99},{"version":"616e09e0cc203f41e18848c4e246d248d94518b080e2b16cf28bc705fb9b35b5","impliedFormat":99},{"version":"518dbebaadc0fc21aa92177d30b043613471869c7fc16502b170c184e60fc17a","impliedFormat":99},{"version":"027b6effcb25477fb182104e60037753a7861782db46001ae54d50b574d0f74b","impliedFormat":99},{"version":"4cad4e814e224a5f6cc40c0b5dfd3c60f6715147a94c27568324439325578c35","impliedFormat":99},{"version":"a6d2eb77f390448a229ca63b4adadbcc549d203b8c038f1f7627a118f732969d","impliedFormat":99},{"version":"1ddd07729c458dd68c47ea2ac1f46f33f64f603f53fe43d74c9bcd65487923fd","impliedFormat":99},{"version":"b886cc0d907b31ae7484a85da375bc8ac42f9986ec1e49c9c7c854956b0a928b","impliedFormat":99},{"version":"69029230a28323de141ad3b08350b45468248d1f8cce9fe67e4c9aed7785fb2d","impliedFormat":99},{"version":"8ab288ca80e5f2a9dab19955fcd9e81a244432cd7d256fdb5ea198ed5a44f0a4","impliedFormat":99},{"version":"0caadd121b8965f24d5c647a2ed01f5fc352772e4cea4bbca2ab78adedfdd921","impliedFormat":99},{"version":"b642b6a4b9b670338bc50281a6dd192fb7cc0b2f9d63d38c250bb1f02e828bbc","impliedFormat":99},{"version":"3c52dce9553224361c062550a63e7ea0c60a26352e87e80ce847134ae2e4d782","impliedFormat":99},{"version":"c2e65a549ac9d227a9002d077af67bb1c77557a72bf39a85c4354831879078ed","affectsGlobalScope":true,"impliedFormat":99},{"version":"38f11ad59dfc38750b789f9f250742d0ce0da44e28f790a032cd15665b70f239","impliedFormat":99},{"version":"c86b1f5777f36dd334a630de065b339f04e4ba672ade417039740847c11246a5","affectsGlobalScope":true,"impliedFormat":99},{"version":"38d2308df413ce823f4764af63dc7e9aa64223f6ba71dbbd5559a6695ca06a08","impliedFormat":99},{"version":"01b1fb6368b7f9128e594c8ad67cbfbfeaed998ba9a59091764f37dc13c867d1","impliedFormat":99},{"version":"a5908d37bbe4df084fa9527e9d672f5d69859684c722e465ca18a6b11f666d7b","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"950f2cd81e30d0ecdf70ab78fcfd85fc5bb28b45ebb08c860daff059feea412e","impliedFormat":1},{"version":"3a5af4fba7b27b815bb40f52715aedebaa4b371da3e5a664e7e0798c9b638825","impliedFormat":1},{"version":"8485b6da53ec35637d072e516631d25dae53984500de70a6989058f24354666f","impliedFormat":1},{"version":"ebe80346928736532e4a822154eb77f57ef3389dbe2b3ba4e571366a15448ef2","impliedFormat":1},{"version":"49c632082dc8a916353288d3d8b2dc82b3471794249a381d090d960c8ceac908","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"71addb585c2db7b8e53dc1b0bcfa58c6c67c6e4fa2b968942046749d66f82e7e","impliedFormat":1},{"version":"c76b0c5727302341d0bdfa2cc2cee4b19ff185b554edb6e8543f0661d8487116","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"e703cfacb9965c4d4155346c65a0091ecded90ea98874ed6b3f36286577c4dde","impliedFormat":1},{"version":"f5ef066942e4f0bd98200aa6a6694b831e73200c9b3ade77ad0aa2409e8fe1b1","impliedFormat":1},{"version":"b9e99cd94f4166a245f5158f7286c05406e2a4c694619bceb7a4f3519d1d768e","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"9563572d81b4aa8b7cf3e24384262341bd53a7a1e5e55034f5d446654b824e8a","signature":"bce0cc5be1903519d3a629f49137b213c22e1d4380a6b4cd409fe5bc38a7dd09"},{"version":"10e44a443fcc02629276b3068a04090ef67456c7b9f49a767e566575625852b7","signature":"2966ffb8b17167488deb293aaa49f5da898066d32ce422afbbf98fc516a42fc1"},{"version":"4d2f510ca3eb24a383eeb142531bf976ffd0e0ea31d960f9b16bc333cda03685","signature":"fc1d8114ef6048cea25504969c001f14e49fc0c90afcce05bc0b465028cdaf71"},{"version":"1fdf5e9f472a68188429b3ba3332c46a9abf74de0e23a43f2f1b4f6579275a9f","signature":"46079b82d154f2c3ae8242a4ef906257a544e05934be47ba373ec035c0248143"},{"version":"9d48cf5ea6f767f9fb3b7a86e9406946936dbbdb1d32c4b9e5c2892928a70db8","signature":"ad0af5ae3b32b4d2e50b0a1fd4b8f38be5da7509962c47bebe76a73ac056b188"},{"version":"5c043033f251a8c5bd7108914393af575650fa9c78a78e548f81d7cfc4014e6f","signature":"74555895068502b65799c3c9928e42f4e56d6f69b021e6f13f294c1a8c354260"},{"version":"b9b5488f9e8043ff017f648745a75b1c2e030a8efa16c6310fa6e6f9e52bcef5","signature":"58969267c23c27443642dec465082e37d7e210fa71c2cf13f516d876ff97b6fa"},{"version":"5275b12537eadeeac78e852fd6ddebb4882770947c64f06bf1bdaf29ecb61303","signature":"c4a90e38375dfd22ab91d9a420cfa703a5dca4145108e0d1106e9873bc96797b"},{"version":"1af38648bd7056b360f5c3607b305f558a09d1d320fa253a7333c169ea9529b1","signature":"08f9b11447950c4f59141f328520c6e31b5a166924336777995a0187f7a4bde9"},"9b1f5337bffec91b0d8559a08d004f2f35357247a6581f8e0650200f344aa1de",{"version":"09ff1fb2ef8f9fa73f08613aa64167219bd12fa732fd9d7bebfe0edabd408d04","signature":"8fad78010d87e993a6a2e50b595266879bc0bfad0209607b0974f0425f71c9af"},{"version":"58483c4909f03f36a319216982788a7d91ca3cf2dd2a341b627136428ae85793","signature":"9d202fb756378d3f79af158bccccd4c6e9b2c525c7e5b6f88bdd4c23c78cfc91"},{"version":"b065b977696599b488831c0189811f198445b31d2684e1017c0dd5a486d7c3ac","signature":"a6785ff8e286cc4b64221c4d9c6a1084f328837d1e3225db9045130dcc8a0ca8"},{"version":"c4a595c3f6c279163c73fde607c056c813ef53f47537bc91b2623dd95f45f2fc","signature":"0dacf73b071d41f5506c1858dbc30a73b85482307cb5e15a63ba4fe654906ce4"},"bb0b00be806f71a00f4a806b9331cc7b919d652d35a53ed9e2774b017dcf1dbd",{"version":"3b96187464c836feb9457f746b1ed44dc91ad76aa65ee8cdd64283b46fda9234","signature":"ec7adc98a93755b7c1ecb861e78423a549a0d8b30751571340bac9c0c974c66a"},{"version":"b4b0f493c3ebf51fc982a1ce59e95b863836dc00c5685d09289cf1953a40fba0","signature":"db39cdaf4c7f6939a6e0533c49464ad9d9a7699adb355247500f92cce14d2a67"},{"version":"b4508ea58d02c3b0931995c9d5a5f464864efa7242243677de743ff9d248ab0d","signature":"02b514f6c19f6e26cd1993dd2504a4d94566ea6af541e90dfcc8ed378c1f0eac"},{"version":"06d0dba413a4d4846e0a93f03e50064f47185987ea23efb769aa14cb22e3e0c8","signature":"83df49fb43b8081fd8c4bcf9ef85f5869b79427dc76a2ac650a94959a1b6a93b"},"5177d8c52d115cda5ab89dcaecbe648a34699b662cecec12d17ba39485b80b92",{"version":"8c72ef1bb77e1dbfe10f37d22327de08b839246785253756944b94c8a328eece","impliedFormat":1},{"version":"26efb34ac3797956919cecf67aa2c229b875620bb7a4eb16e3ab97e2fafe0c76","impliedFormat":1},{"version":"4b90c2e95674469fb79ad5c7f2fd946304acc2910bf578f7b1929fead92a1d27","impliedFormat":1},{"version":"6ba3836f4607d0e8a2da4d7a47cd9a5750703d69dd07281997c39831a2edbade","impliedFormat":1},{"version":"56024065be958aac94c30535a04a6bd8db908b2924a21011009291353fcd2478","impliedFormat":1},{"version":"ac339e7b2a6953534dc34b2c76b5128c84eb33b947e9c14b0c6f819e8974842f","impliedFormat":1},{"version":"33c337de15324843b658d00995db674f9c618fe9ad6ad6b43f0fe6835984fa0b","impliedFormat":1},{"version":"27254b3eb6ba4c0630413a63892534ecbac47746bed81f72d996dca1662936d4","impliedFormat":1},{"version":"7e687be116d78dd903a427dacc3cbad3ff9f7ace60dffa8c83b7102ded92de47","impliedFormat":1},{"version":"3da682a8d8b2cf020828199e06d40f4b8f617d8582ab466354b804602e492b60","impliedFormat":1},{"version":"cca10549035d257a89fc46649190b96534af65715aa85b57f0d2a5c61fb98938","impliedFormat":1},{"version":"5e04be59bf10e4d8adf988610f7e80864f33e7e6dd5072ce4af354305581dec9","impliedFormat":1},{"version":"6cf6a4290ee17a9104da299a794d39fb02ef19e054ab14f2ec1d2753514f131b","impliedFormat":1},{"version":"06648b76a5d56f77a1443626ced0fe0fa0dc58827df8ad6195e331915a46e347","impliedFormat":1},{"version":"abd934f6207c5954c02630d92160c4d866f0664047dae0a939f9bdcdb8302e98","impliedFormat":1},{"version":"66c15e0c870a2928dc58a7b19762e94a32ff4ebe4a64b42e9419d212e51cf2c8","impliedFormat":1},{"version":"e5d037491332f872acd558856a11b23c4d5bf726ca3cb9208c7ab8051c4b0faa","impliedFormat":1},{"version":"157227cf6b14eee7ee92a96862902be7d64a6bda2befecf6ebf80e97079e1545","impliedFormat":1},{"version":"a4bd311189ce445fa39d86b23905683a6cd6d9eb884bf8a100329b3dbfb1db43","impliedFormat":1},{"version":"f4d06ad1b70434339a4fdf464ac4d804154c71314c3e2fb928bead12b620cf1a","impliedFormat":1},{"version":"c3878889468470079a7a3f91ab157b2c15da1191d9253a3149eedf4b7c7e9712","impliedFormat":1},{"version":"b2ef09a1454cd38bec48a17316abe8c6b1d56711b1071e5974ed5c377741131b","impliedFormat":1},{"version":"56290b22f633dc5afd255ed0e897d1e8e6567fa5abf38773c16ba7b89de4c56a","impliedFormat":1},{"version":"698b7a228b992346acdd678b918804f4e68824fbcb5bfa551603aa6d4217548d","impliedFormat":1},{"version":"dd1a80df85e075e40a7edd6d9a549a2a954956bfbdd61556609394275cc78895","impliedFormat":1},{"version":"e26c74e599a55bda4bcdb20a9d5caa28728073451944e358f9d5de19c77fe408","impliedFormat":1},{"version":"89d717a0978df19ddbcdc62e43b2972e8c374de52518c2e8ac8a43992b76533a","impliedFormat":1},{"version":"67d4363beaca9c0ba210bbfc33fbef91d1c22c77d5717aca547dd8b650fd0fbc","impliedFormat":1},{"version":"47175f6d1984ee2663d867225c642bc2645d2b37508f0a2b0693cbcd38bf1b8f","impliedFormat":1},{"version":"e5bfab23943623a63a9612d1efd4b9cbae9abe99d88286246fc4ad9564b8078f","impliedFormat":1},{"version":"192d1e66dfb3f57d196fd9db6576d43a8a57af0ec7995eae139bbbc8270427ec","impliedFormat":1},{"version":"ee03033435713344042f5ee1b2f5573f988462c3c6c7dabd4a62df9830bcdd23","impliedFormat":1},{"version":"bfca9c2cc975dd04310c02d200c61baac194b8ebb52d26ea83c50092ae005f88","impliedFormat":1},{"version":"bcd9b17745bc73031b9c23b834dfdc411dec8f6309c5c3ed5dfc05ad9271f2b3","impliedFormat":1},{"version":"a8114e7ae77ce8038227e4b5ffe064b12d2f228e42bf090bbc8a43e4b36774fa","impliedFormat":1},{"version":"123cd45999e04cbd809b33dd14b83a7cdf5bd42ef5419b9dd912f13408fa72be","impliedFormat":1},{"version":"6db08c20642e5c3b7b395d29c53380265aa22a5e9409df7e67b27194f5068369","impliedFormat":1},{"version":"06944bcadb20a137ec5cccefa6d02beb73f5d2889a3dd6409e704acdcd05030f","impliedFormat":1},{"version":"9e21f2c882a1ee090d1d72e7d48b0f4c338ebcc643f684786f8f71ea317d0761","impliedFormat":1},{"version":"4af5e1d11bed0c5594ada90b8169a367bb0622692d904d04e7b0e0fb51b4563a","impliedFormat":1},{"version":"ab428a4142c089f84f277bb4aa7d514f454b69b67a47b9226804178c50ea9b88","impliedFormat":1},{"version":"f2ecf41bea9d32be3e27c71b7d1d9e3f58083b45fd36bd25980899a1168ee109","impliedFormat":1},{"version":"0adcb5db0aa0de1425495b135dacaf6f2240e3ad48b905cd8139f730fedaa4ca","impliedFormat":1},{"version":"116fd7b3ef6684531a1590b36a3ea2d0cd5383230d8509b928e567c08697a3ee","impliedFormat":1},{"version":"ee9d7a75e75cc2a61d2c781df3864e5a0d4f3dbd149ed22c65749bd733375d88","impliedFormat":1},{"version":"35161f13181515b62e1d83bbf6c4062dd64df50b943b11b512bde6e3b4c7f4c9","impliedFormat":1},{"version":"7827df7348614d14296887c07549eab5c11dba6f201519eae67ac811d63a24fb","impliedFormat":1},{"version":"5199dc536c144915831cdf5569baab87db25e7926e82af048b28adc2044f1e5c","impliedFormat":1},{"version":"ea9a572684e0f6bdf35740966b2aaaf341a49ff0eaa0d25f3113f518741d0eff","impliedFormat":1},{"version":"1ee3efb6da370bd447ba467b5edb6f79ecf4082eeda41e201112b4bba48cd9e7","impliedFormat":1},{"version":"f6650f06155f0147cb49d8230ac3d07ac7fc5d38fb5f5db23ca94aebf9a2b240","impliedFormat":1},{"version":"76eb98cfa62a7c06c358c566646ad1bb030b35df08fc20f77f63d733621a9b1d","impliedFormat":1},{"version":"118d80f9540cfa2b110c2835648059d35660b02250e6ac64b7d81791ccb5a5e1","impliedFormat":1},{"version":"5aeb1520ee864150fd128807768ecff927b5b716be83da72869936a8ca723ae7","impliedFormat":1},{"version":"2ad51cec69437b45a5135e5cafbd8e0bbf361e352c65c02ba57ab190e5cf111f","impliedFormat":1},{"version":"375a1e03a1d0c742157b80591ebabab92929be9d3329eac73269a364dcec37be","impliedFormat":1},{"version":"7a91879080f4028bd9a3e76a299f356fa4b1440a36953489c1c0de570529a19b","impliedFormat":1},{"version":"4a6c26f345674f06453339314f10cc9c2e0b97ef789682d743327fb7b9ea2838","signature":"351477ac802666545603bfba7e9ceff76f00da4a52427c878cff1e6c881df4b5"},{"version":"2940a44d9c23110d9dc2fa75a81c5312a9f52116b429b9c0f9d9e5e6f0ff74a9","signature":"d0c08cc38b8d1f405d3972d750f4bd01394345a4f1f549a4bb9e421a480e613c"},{"version":"d95953d2fb0c059266530bf031b958b9ac518c0cb4812e1b0c7baf16a739853f","signature":"d934a3c076ebaef0f56281484d7b88d333cf787ca2e4403192003fe746897b11"},"1affef5e5a5f57615fb044f0a33077838cccb4f8382a311bb037084b0a091f77",{"version":"8079971ea09988a65bd425cbd4e81b875e0129f39d5e3dc37a6ad982d66d256b","signature":"9b3a62e9c0d0a9bb8f4f3960f68fcc871e793241b177793eb1b74fdf8eaca25f"},{"version":"bae24a77dc33ee3a80cbaf2dfbd7f7c6acb6edde4bff39163a3058c93c14e0d2","signature":"f28363132526035687a0b6b8424d02f7bcc4b73f83b3ab12d6fa9adc51f9b814"},"df2a52a6abb404f784afb39222e74db1074e040bdb1a62813b841d4f9d695438",{"version":"e5259ff908cd94f870b3aca05a2e2b5331da3b4d4b833e0e44a0051ac438b7ee","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"79e16f645be25b58b2aba1a34c0cccf312eee59e99a37a2e1c32ff2f5b5f9fa0","impliedFormat":1},{"version":"2aee4866aaadd28a511b8ccbbf5de09b05d3fef4c3d4e2b925df67bd56379e32","impliedFormat":1},{"version":"567e539302fcf0cff85868ba28b1d377e577b7df2f9bd8f15ff294db6eab8f41","impliedFormat":1},{"version":"092fe093266abdd6a1d66bf0b7a5d6abbdcf7e860dfc0c8e0de6d4e262b147b6","impliedFormat":1},{"version":"d00cd46c963aa004e009458cae42d93d98266f82842f08836701b0abc7eaffa2","impliedFormat":1},{"version":"9d6edb8d1eef6ecbb7d5c031fd810b6fbd20f938ca36d620e1ff6f715ea7bdea","impliedFormat":1},{"version":"bd19825b929ed02095afc0d3353f270ca7ce5143055753174153d59a2cf76da2","impliedFormat":1},{"version":"95c11dddc8f61bfbf8e041e657d3f23d60db75fca921ad12ec86e479783f3734","impliedFormat":1},{"version":"0034b70dbb922bb3eb6102957948969d28a824256b48f5645df2ae7ef0f4fd3e","impliedFormat":1},{"version":"395225adaef9e8588113a04f07b6b387068cd8e7a0230cb059750653808669f2","impliedFormat":1},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"b406a701b85ba51c1e228606dc0e24fbe552df32935965bf34abe698554c0027","signature":"8b1f3922430b4de1ea290dc7219afc7023b5a8f4b5126c0bc2fb67d72239e667"},"ee083b355085d100be23d1decf29d53d4ddefdda6ae215abc7cbbf131735ceef",{"version":"571c9e6d4ec3635ef131f0767347d20e4c7868a2cd3ce56c6fb5f1c15f0f9c35","signature":"99a775f82d53af53fbd4340ea421d16d13698ce4108a268748ca2ba9c4ef5df3"},"4adacdd10660b791915a8c65ed0d10e65b3efe8efcc6adad409ee58408c27e29",{"version":"5d3e73857630bf788ead3d85f3ce9cbffd58501b7ec0df5a091f36dd7a5dff35","signature":"fcfa54765a6fc0a8f6131817215f0c174f2e4cc12ca05cd5a30980f1bd10453e"},{"version":"d51ce83f7eea5362f73585a4ec7ebf6f5774ad87dee6e880e17c0edd8d5d7a11","signature":"1a83d67b08524d4f1719c1e76cd5a6fd1ed58f3104e662dd9c0fd7569b30bdf7"},{"version":"ba82079753d7e454cf3b2a466960f90e2d645f54fb842a5d2bf8582d6398fb59","signature":"6801339821a88f89e3da45a07c2788c796f39159ab4a218a5c5622ec347aac60"},"4854ae5d9afcc0b47241b403a52eb2d9604755cd360d765eee87eebda2cab336",{"version":"b8d099fb63190d0c37a7000a2b8071311a3283e586fab8b716276ad7ca114320","signature":"ba40efe2d4b63d17837dfa911cc0a780c7ff1525a753edb7dd475a44acd3b74a"},"09862b74511910b955a0fb69a1dbb5f8906fe6f194a6079e4d61f8b77dd00ea0",{"version":"5603469ac3dece937750f0813773a621ee6bbdb157f920b90252edb63efd6709","signature":"42e11e0395b48c7f0fdcc839b3556e61da7350e863a2b2d38016a2ced0d42c2a"},"dd5daf84e0584133c7bcbb7d4952ea2d003d39d06ae3df29903c121004199212",{"version":"73b47eba1b89d8932c9c1386ececd401508ba7a57c64faea6f702891e8030c59","signature":"1b16fad6ccf0d677dba98f79ca9c082d61b667e7fecf5a2569b0bd73cbf6e7d0"},"24e78319c562dbcb524ab159ba98032eb5469d15077c95ec4844eb0c06b8e3d0",{"version":"aefac0e44e7040835309e120bf4e93a5955a3ca552ab5440c2db3d96b1dc90a9","signature":"0946a3931a4f5318dccd7dfe7393b40292a0fd4464b61c6276233c5dd9cbeb9a"},"8e50584d81029a0f06d91becc711ecef78f147a6e6f42397551aff560457f6b1",{"version":"bd0e4be8ab627f0dfba25c7cafe5917faf502a874bf15e85d438101e7a6d94dd","signature":"dff0fd676ac8ae89e2135bea132e049e613fd47eea581075f82465845eedbb96"},{"version":"8b7f1430b8036843e961d0a8e8d404acfb8de20ad9804b5ba249bae7c812d5da","signature":"4333bc1154e167925ee4b7ae1d09031b02be4b33ed88fe56180e680e8886cbbf"},{"version":"91a09454a027d96924f4f3c3cf6468b33660236f9c2da44221f14dbbfd23df52","signature":"8216aee5d81ec63248ba300134bb4a1901b789f891940bc9ff8ad00890360c26"},"3b5660e245775469d77072ab4d9dfc1dd55184b8d883af4d2a9d89c5053855b5",{"version":"91b4ce96f6ad631a0a6920eb0ab928159ff01a439ae0e266ecdc9ea83126a195","impliedFormat":1},{"version":"e3448881d526bfca052d5f9224cc772f61d9fc84d0c52eb7154b13bd4db9d8b2","impliedFormat":1},{"version":"e348f128032c4807ad9359a1fff29fcbc5f551c81be807bfa86db5a45649b7ba","impliedFormat":1},{"version":"42f4d7040a48e5b9c9b20b5f17a04c381676211bdb0b5a580a183cf5908664be","impliedFormat":1},{"version":"d4e4fbb20d20cc5b9f4c85f2357f27cb233cd01f8ca6d85dcca905ec15143e06","impliedFormat":1},{"version":"c2fc483dea0580d1266c1500f17e49a739ca6cfe408691da638ddc211dfffad0","impliedFormat":1},{"version":"dfc8ab0e4a452b8361ccf895ab998bbf27d1f7608fae372ac6aa7f089ef7f68d","impliedFormat":1},{"version":"cca630c92b5382a0677d2dedca95e4e08a0cae660181d6d0dd8fd8bdb104d745","impliedFormat":1},{"version":"2ba3b0d5d868d292abf3e0101500dcbd8812fb7f536c73b581102686fdd621b4","impliedFormat":1},{"version":"c16c3b97930e8fbf05022024f049d51c998dd5eb6509047e1f841777968e85c1","impliedFormat":1},{"version":"cce15e7530c8062dea0666a174f31c1fe445a97357885480748b072778fc6f36","impliedFormat":1},{"version":"535b2fc8c89091c20124fe144699bb4a96d5db4418a1594a9a0a6a863b2195ae","impliedFormat":1},{"version":"dd5165bf834f6e784b4aad9fae6d84307c19f140829e4c6c4123b2d1a707d8bd","impliedFormat":1},{"version":"7ccf260729e19eed74c34046b38b6957bcfe4784d94f76eb830a70fc5d59cb43","impliedFormat":1},{"version":"21575cdeaca6a2c2a0beb8c2ecbc981d9deb95f879f82dc7d6e325fe8737b5ba","impliedFormat":1},{"version":"00343c2c578a0e32ecc384ed779ff39bc7ec6778ef84dc48106b602eb5598a6c","impliedFormat":1},{"version":"c333b496e7676a8b84c720bdece6c34621e3945b7d1710d6ed85d8b742852825","impliedFormat":1},{"version":"3eb7d541136cd8b66020417086e4f481fb1ae0e2b916846d43cbf0b540371954","impliedFormat":1},{"version":"b6fed756be83482969cd037fb707285d46cbb03a19dc576cff8179dc55540727","impliedFormat":1},{"version":"26602933b613e4df3868a6c82e14fffa2393a08531cb333ed27b151923462981","impliedFormat":1},{"version":"8fc19c7114cfd352ff9fb615028e6062cb9fa3cd59c4850bc6c5634b9f57ea27","impliedFormat":1},{"version":"05942150b4d7e0eb991776b1905487ecd94e7299847bb251419c99658363ff84","impliedFormat":1},{"version":"073c43eff28f369a05973364a5c466859867661670eb28e1b6f3dd0654dd0f0e","impliedFormat":1},{"version":"4a7c3274af9c78f7b4328f1e673dec81f48dd75da3bc159780fb4a13238b6684","impliedFormat":1},{"version":"1134991f69fff6f08bd44144518ae14bc294d6076dba8a09574ae918088c5737","impliedFormat":1},{"version":"259a3d89235d858b3d495dc2d1d610d6ce4ac0e91da1ae6a293f250d895d45dd","impliedFormat":1},{"version":"369b7270eeeb37982203b2cb18c7302947b89bf5818c1d3d2e95a0418f02b74e","impliedFormat":1},{"version":"f4c772371ce8ceaab394e1f8af9a6e502f0c02cbf184632dd6e64a00b8aeaf74","impliedFormat":1},{"version":"039bd8d1e0d151570b66e75ee152877fb0e2f42eca43718632ac195e6884be34","impliedFormat":1},{"version":"89fb1e22c3c98cbb86dc3e5949012bdae217f2b5d768a2cc74e1c4b413c25ad2","impliedFormat":1},{"version":"855b9b5ec5cc534fcf9c2799a2c7909b623fcb4ed1a39b51d7c9f6c38adec214","impliedFormat":1},{"version":"58da08d1fe876c79c47dcf88be37c5c3fab55d97b34c8c09a666599a2191208d","impliedFormat":1},{"version":"7e541f14d37444b3f3c3580ed807c36733f9c9137fe7297f46732a459b7e2739","signature":"754cfa02470aa619c32f0bfcd45bd79448307dc2084d91faba186b22614a2bc5"},{"version":"ae3da770eeee627cdae660f56b9caddd0461aa044180b71c647d530c8eeb8403","signature":"d6de841d6c0b6cdbe1fb3bab69c5a822db427a38112c3bbed97bcdbf133d5791"},"bbab50527f325d4e80133ce651f64ea3dc1f27399b5735c52f08129b78c8bf09",{"version":"38269b55458b54b8a6d555c68861332f7b77ff117142150ff8826e66603098fa","signature":"bfc4ca2b4e87103961ef1d70f04980c21a27262a66a6551491b0ced2fcfa9102"},{"version":"df40cc3cbe86197d5e37cc4587c577e061e43dfb43ae6e008778bae4335540d6","signature":"24407ccc87811662c18036c5abf7cee2ced3b225c74349062164099d98a8a819"},"7f02d3dd69e1ac0b5bc3273cf85bff845aded8c19bf9941fa81543aaf1aa514e",{"version":"d3fda1b56c7504b55de0ce37d2ad5650281df47a2c9f2ca7cd0f2c579a8f2e43","signature":"337eff7966a27d188fb47faa58b738791db31e69755c4f3edb74424bd58c4e8a"},{"version":"cbae4ef4a9000af7e553d0a4fc79b191df7c0b9aad7a7db8771a3aa8cb1c00e5","signature":"1b68b44bf416d5ac5f36c59a998dab6f33efd46b8812d1c46588176e4454601a"},{"version":"28df56ca2d784df73ac68181c83aeadafd6ded9cdae6847f53f4359e6e9baea2","signature":"1e40c707386547b85eaed90f0a540905e784effbafa767e18435c270e9e3d097"},{"version":"b2cd408850ad9df8a41aa93085c6fbf47ed5aec4154dc59b8837f8b4d700ef24","signature":"a40544dac75a719ea82fe76ec1002be6dce9d2f7eef8d867c2bed8eba533c40c"},{"version":"e4aef6fa7bad1326f8edc066ad796a1634aaec954a97b878d46a7804c73bfdb5","signature":"ce5775cb39c31f310b1abcade60187859a376af581aeb9cb644ce7bd2fc158c8"},"d381452bb935754c0c86bf185f1cecd58800e3cf9147748c8ec80a92fc852e6e",{"version":"2971e28479774838c8f06bc7fa913924106d45877c58da7f2a4b97d6bd76516d","signature":"56055c89baec056fa1aa797807cf5b6f9bd3a0f322d4101d7cd411fedbe47037"},{"version":"92ec74a116d454015264ec3a1238416c800b352c423b0773deee6f5ed6ddce8b","signature":"4536241a2b6b50c093c96b45b6249d9bf51ad460b8a5f5b67b7e012e0d03c05d"},"5b583a7f8b2fc3c7f29e8aebc73c99cbe8df5f6e59a0c2427f90e89f8bdfb13d",{"version":"a2eb6498e964b2418d0d178da23c426dbd28eaa8392aab8d7efaacddea9a5b9f","signature":"e0ab3680a5936bf24b5081dd0ae5b003908dfc8e4084ce97933a95ac96040122"},{"version":"590cdcbea0c9d5bfa62a9b76fc1ad7a2fba8b14a43c08ea8cc3bde29911cbe62","signature":"3ad4ae570b7091713c30b128d700820867f667b6f760f68250d614b2af2856ce"},"c060799d029416006ea24e10c86c8f1cdf2bb32b6fa2478cb4a941798bbb15f7",{"version":"616a980aca196d3779b5b483ddba67c2ce2f7ae0a3c344dd13b400de2e601d7e","signature":"7ad89fdfda1b7df8a652192903101ed2d98e276564d402da7651d72f84f7060e"},{"version":"a3e4d5b357b8059bbe77f1ba4e1b5ac418c887391ac17f752c06fb45750de0e4","signature":"5cf2e7e1c2638fb166e627277ba741ed1fb72ab8112675c1b5ecacea1bdadae9"},{"version":"b8d3773a936faaccb054f7ab53a3794816556df48fc7ff443749e32eba63b94a","signature":"80f5c86c4ec93e5e7eb0110dc1ba5d85115455d39eed616aaa44ae4d58cbc613"},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"19efad8495a7a6b064483fccd1d2b427403dd84e67819f86d1c6ee3d7abf749c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eef826bc4a19de22155487984e345a34c9cd511dd1170edc7a447cb8231dd4a","affectsGlobalScope":true,"impliedFormat":99},{"version":"6bfdbd285cb25907f31ba9db446e61eb2aa55cf46220ebe5fceaa18b2a0b26e8","affectsGlobalScope":true},"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","9e5985581e4bf2d7d7ebebc5464404da39c4eb552ee07bcbceb669c108352a99","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",{"version":"0b026968c66026a85c16aab5581ca03688da142dbc2bc5afcbaec968a8d27350","signature":"1c57ca6c8cb5921c3246069f61d6479fcb659bb2de7406c009aad991aa1c3b9b"},{"version":"c536f8088dfa642e704423872fd8017c3049f6ee2046465ea879494cae0650f1","signature":"2e207df875878acbb125a490076d902de5bc8606b510abb58175ed37c79f9746"},"3ffc7b76098f0c4688a57b9a2948efff274e65571b871f3f6dcc43786a0eddee",{"version":"55aec2fd68e2abfa451013955bd6cac5618f5bc814cb0131d9c3d0e9283dfafa","signature":"8f42eae87de9268f64d1e20cb3359677a1ca99dc0205c993ca00747667674489"},"e3f335a00c1b2165f20e125cb29536b75b4fcc95c1760f2803b95feb2ded15bd",{"version":"73a0ee6395819b063df4b148211985f2e1442945c1a057204cf4cf6281760dc3","affectsGlobalScope":true,"impliedFormat":1},{"version":"d05d8c67116dceafc62e691c47ac89f8f10cf7313cd1b2fb4fe801c2bf1bb1a7","impliedFormat":1},{"version":"3c5bb5207df7095882400323d692957e90ec17323ccff5fd5f29a1ecf3b165d0","impliedFormat":1},{"version":"7959556e748fee75a1ed10822bed4e92edae99ed75e428b650376bacb20ce257","signature":"5a81de12417101e8964d1370944563204a470760be39079613c910b549a0b53a"},"4ca18ab8cd76fb5fb9f7013772f05cdf7b17cdd2f65c39a20399c01c04952a9e",{"version":"a0acca63c9e39580f32a10945df231815f0fe554c074da96ba6564010ffbd2d8","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"3b724a66c071d616203133f8d099a0cb881b0b43fd42e8621e611243c5f30cd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a38efe83ff77c34e0f418a806a01ca3910c02ee7d64212a59d59bca6c2c38fa1","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"3fe4022ba1e738034e38ad9afacbf0f1f16b458ed516326f5bf9e4a31e9be1dc","impliedFormat":1},{"version":"a957197054b074bcdf5555d26286e8461680c7c878040d0f4e2d5509a7524944","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"e9b97d69510658d2f4199b7d384326b7c4053b9e6645f5c19e1c2a54ede427fc","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"f478f6f5902dc144c0d6d7bdc919c5177cac4d17a8ca8653c2daf6d7dc94317f","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"b200675fd112ffef97c166d0341fb33f6e29e9f27660adde7868e95c5bc98beb","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a0a1dda070290b92da5a50113b73ecc4dd6bcbffad66e3c86503d483eafbadcf","impliedFormat":1},{"version":"59dcad36c4549175a25998f6a8b33c1df8e18df9c12ebad1dfb25af13fd4b1ce","impliedFormat":1},{"version":"9ba5b6a30cb7961b68ad4fb18dca148db151c2c23b8d0a260fc18b83399d19d3","impliedFormat":1},{"version":"3f3edb8e44e3b9df3b7ca3219ab539710b6a7f4fe16bd884d441af207e03cd57","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"d71535813e39c23baa113bc4a29a0e187b87d1105ccc8c5a6ebaca38d9a9bff2","impliedFormat":1},{"version":"8cf7e92bdb2862c2d28ba4535c43dc599cfbc0025db5ed9973d9b708dcbe3d98","affectsGlobalScope":true,"impliedFormat":1},{"version":"278e70975bd456bba5874eaee17692355432e8d379b809a97f6af0eee2b702d8","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"636302a00dfd1f9fe6e8e91e4e9350c6518dcc8d51a474e4fc3a9ba07135100b","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"e1ce1d622f1e561f6cdf246372ead3bbc07ce0342024d0e9c7caf3136f712698","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"27e4532aaaa1665d0dd19023321e4dc12a35a741d6b8e1ca3517fcc2544e0efe","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"8c2ad42d5d1a2e8e6112625767f8794d9537f1247907378543106f7ba6c7df90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"98ffdf93dfdd206516971d28e3e473f417a5cfd41172e46b4ce45008f640588e","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"12e8ce658dd17662d82fb0509d2057afc5e6ee30369a2e9e0957eff725b1f11d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74736930d108365d7bbe740c7154706ccfb1b2a3855a897963ab3e5c07ecbf19","impliedFormat":1},{"version":"858f999b3e4a45a4e74766d43030941466460bf8768361d254234d5870480a53","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"63b05afa6121657f25e99e1519596b0826cda026f09372c9100dfe21417f4bd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"3797dd6f4ea3dc15f356f8cdd3128bfa18122213b38a80d6c1f05d8e13cbdad8","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1}],"root":[[157,176],[234,240],[254,273],[306,326],[333,343],347,348],"options":{"allowImportingTsExtensions":true,"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"esModuleInterop":true,"jsx":4,"module":99,"noFallthroughCasesInSwitch":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"strict":true,"target":7,"useDefineForClassFields":true},"referencedMap":[[270,1],[271,2],[272,3],[273,4],[238,1],[240,5],[239,6],[334,7],[335,7],[261,8],[259,9],[260,10],[308,11],[306,12],[307,13],[326,14],[322,15],[323,16],[319,7],[320,17],[317,18],[316,7],[312,19],[315,15],[324,20],[321,21],[314,15],[318,22],[313,15],[325,23],[236,24],[237,25],[235,7],[269,26],[268,9],[336,7],[254,27],[255,28],[256,29],[257,30],[262,7],[263,31],[264,15],[265,32],[311,33],[310,34],[267,35],[266,15],[337,36],[309,37],[338,7],[258,38],[341,39],[340,40],[339,29],[333,41],[342,7],[343,42],[305,43],[304,44],[62,45],[142,46],[140,47],[141,48],[125,49],[93,50],[104,51],[105,52],[106,53],[126,54],[107,55],[128,56],[108,57],[102,58],[111,59],[110,59],[109,60],[123,50],[103,61],[112,62],[117,63],[113,50],[124,7],[127,7],[114,49],[92,64],[94,49],[101,65],[115,66],[121,45],[95,67],[97,67],[120,67],[96,67],[116,49],[98,67],[100,49],[118,49],[119,67],[99,67],[122,59],[78,68],[61,69],[91,70],[65,71],[64,72],[75,45],[68,73],[67,74],[79,75],[80,45],[74,76],[69,45],[71,77],[66,78],[77,79],[70,80],[89,81],[81,82],[73,83],[76,45],[82,84],[90,85],[88,86],[87,86],[86,87],[85,86],[83,86],[84,86],[63,74],[72,88],[57,89],[58,90],[60,91],[59,92],[56,93],[55,45],[394,94],[395,94],[396,95],[355,96],[397,97],[398,98],[399,99],[350,45],[353,100],[351,45],[352,45],[400,101],[401,102],[402,103],[403,104],[404,105],[405,106],[406,106],[408,107],[407,108],[409,109],[410,110],[411,111],[393,112],[354,45],[412,113],[413,114],[414,115],[447,116],[415,117],[416,118],[417,119],[418,120],[419,121],[420,122],[421,123],[422,124],[423,125],[424,126],[425,126],[426,127],[427,45],[428,45],[429,128],[431,129],[430,130],[432,131],[433,132],[434,133],[435,134],[436,135],[437,136],[438,137],[439,138],[440,139],[441,140],[442,141],[443,142],[444,143],[445,144],[446,145],[52,59],[349,59],[48,45],[50,146],[51,59],[253,45],[49,45],[346,147],[345,148],[344,45],[241,59],[252,149],[246,45],[247,59],[245,45],[250,45],[248,150],[249,45],[242,45],[251,45],[244,151],[243,45],[204,152],[231,153],[205,153],[206,153],[193,152],[207,45],[181,152],[208,153],[209,45],[210,152],[211,153],[212,153],[213,45],[214,45],[224,153],[215,153],[192,152],[216,153],[217,45],[218,45],[219,45],[220,153],[194,153],[182,153],[196,153],[197,153],[198,154],[199,153],[200,153],[203,155],[178,45],[232,156],[233,153],[186,153],[190,153],[188,153],[180,45],[221,153],[187,153],[191,45],[222,153],[227,153],[223,153],[184,45],[189,153],[202,153],[225,153],[201,153],[226,153],[195,153],[185,152],[228,153],[183,45],[229,45],[177,45],[230,45],[179,45],[274,45],[289,157],[290,157],[303,158],[291,159],[292,159],[293,160],[287,161],[285,162],[276,45],[280,163],[284,164],[282,165],[288,166],[277,167],[278,168],[279,169],[281,170],[283,171],[286,172],[294,159],[295,159],[296,159],[297,157],[298,159],[299,159],[275,159],[300,45],[302,173],[301,159],[139,174],[130,175],[138,176],[137,176],[129,45],[136,177],[131,178],[132,179],[134,176],[135,180],[133,178],[53,45],[54,45],[46,45],[47,45],[8,45],[9,45],[11,45],[10,45],[2,45],[12,45],[13,45],[14,45],[15,45],[16,45],[17,45],[18,45],[19,45],[3,45],[20,45],[21,45],[4,45],[22,45],[26,45],[23,45],[24,45],[25,45],[27,45],[28,45],[29,45],[5,45],[30,45],[31,45],[32,45],[33,45],[6,45],[37,45],[34,45],[35,45],[36,45],[38,45],[7,45],[39,45],[44,45],[45,45],[40,45],[41,45],[42,45],[43,45],[1,45],[371,181],[381,182],[370,181],[391,183],[362,184],[361,185],[390,186],[384,187],[389,188],[364,189],[378,190],[363,191],[387,192],[359,193],[358,186],[388,194],[360,195],[365,196],[366,45],[369,196],[356,45],[392,197],[382,198],[373,199],[374,200],[376,201],[372,202],[375,203],[385,186],[367,204],[368,205],[377,206],[357,207],[380,198],[379,196],[383,45],[386,208],[332,209],[328,210],[327,45],[329,211],[330,45],[331,212],[156,213],[147,214],[154,215],[149,45],[150,45],[148,216],[151,213],[143,45],[144,45],[155,217],[146,218],[152,45],[153,219],[145,220],[163,221],[164,45],[166,222],[165,45],[157,45],[158,223],[347,224],[348,225],[234,45],[161,226],[172,227],[173,226],[176,228],[174,45],[175,229],[160,230],[162,226],[167,231],[170,232],[159,233],[171,234],[168,235],[169,230]],"semanticDiagnosticsPerFile":[[238,[{"start":59,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305},{"start":167,"length":10,"code":2339,"category":1,"messageText":"Property 'currencies' does not exist on type '{ accounts: string; auth: string; entries: string; transactions: string; user: string; }'."}]],[239,[{"start":9,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305}]],[306,[{"start":124,"length":11,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'AccountType'.","category":1,"code":2305},{"start":137,"length":8,"messageText":"Module '\"@ledgerly/shared/types\"' has no exported member 'Currency'.","category":1,"code":2305},{"start":1728,"length":8,"code":2322,"category":1,"messageText":{"messageText":"Type 'Resolver<{ type: AccountTypeValue; name: string; commodityId: string; initialBalance: string; description?: string | undefined; }, any, { type: AccountTypeValue; name: string; description: string; commodityId: string & BRAND<...>; initialBalance: string & BRAND<...>; }>' is not assignable to type 'Resolver'.","category":1,"code":2322,"next":[{"messageText":"Types of parameters 'options' and 'options' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'ResolverOptions' is not assignable to type 'ResolverOptions<{ type: AccountTypeValue; name: string; commodityId: string; initialBalance: string; description?: string | undefined; }>'.","category":1,"code":2322,"next":[{"messageText":"Types of property 'names' are incompatible.","category":1,"code":2326,"next":[{"messageText":"Type 'string[] | undefined' is not assignable to type '(\"description\" | requiredKeys<{ commodityId: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>)[] | undefined'.","category":1,"code":2322,"next":[{"messageText":"Type 'string[]' is not assignable to type '(\"description\" | requiredKeys<{ commodityId: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>)[]'.","category":1,"code":2322,"next":[{"messageText":"Type 'string' is not assignable to type '\"description\" | requiredKeys<{ commodityId: string; description: string | undefined; initialBalance: string; name: string; type: AccountTypeValue; }>'.","category":1,"code":2322}]}],"canonicalHead":{"code":2322,"messageText":"Type 'ResolverOptions' is not assignable to type 'ResolverOptions<{ type: AccountTypeValue; name: string; commodityId: string; initialBalance: string; description?: string | undefined; }>'."}}]}]}]}]}},{"start":1822,"length":12,"code":2339,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'AccountDomain'."},{"start":1922,"length":14,"code":2322,"category":1,"messageText":{"messageText":"Type 'number' is not assignable to type 'string & BRAND<\"AmountString\">'.","category":1,"code":2322,"next":[{"messageText":"Type 'number' is not assignable to type 'string'.","category":1,"code":2322}]}},{"start":2587,"length":16,"code":2345,"category":1,"messageText":{"messageText":"Argument of type '(data: AccountCreateDTO) => void' is not assignable to parameter of type 'SubmitHandler'.","category":1,"code":2345,"next":[{"messageText":"Types of parameters 'data' and 'data' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'TFieldValues' is not assignable to type 'AccountCreateDTO'.","category":1,"code":2322,"next":[{"messageText":"Type 'FieldValues' is missing the following properties from type 'AccountCreateDTO': commodityId, description, initialBalance, name, type","category":1,"code":2739,"canonicalHead":{"code":2322,"messageText":"Type 'FieldValues' is not assignable to type 'AccountCreateDTO'."}}]}]}]}},{"start":4205,"length":15,"code":2345,"category":1,"messageText":"Argument of type '\"currency_code\"' is not assignable to parameter of type '\"type\" | \"name\" | \"description\" | \"isSystem\" | \"commodityId\" | \"initialBalance\"'."},{"start":4499,"length":12,"code":2339,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'FieldErrors'."},{"start":4565,"length":12,"code":2339,"category":1,"messageText":"Property 'currencyCode' does not exist on type 'FieldErrors'."}]]],"affectedFilesPendingEmit":[270,271,272,273,238,240,239,334,335,261,259,260,308,306,307,326,322,323,319,320,317,316,312,315,324,321,314,318,313,325,236,237,235,269,268,336,254,255,256,257,262,263,264,265,311,310,267,266,337,309,338,258,341,340,339,342,343,163,164,166,165,157,158,347,348,234,161,172,173,176,174,175,160,162,167,170,159,171,168,169],"emitSignatures":[157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,234,235,236,237,238,239,240,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,334,335,336,337,338,339,340,341,342,343,347,348],"version":"5.8.3"} \ No newline at end of file +{"fileNames":["../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react/jsx-runtime.d.ts","./src/app.tsx","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/client.d.ts","./src/main.tsx","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/hmrpayload.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/customevent.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/hot.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/importglob.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/importmeta.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/client.d.ts","./src/vite-env.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/typealiases.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/util.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/zoderror.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/locales/en.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/errors.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/parseutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/enumutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/errorutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/helpers/partialutil.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/standard-schema.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/types.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/external.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/lib/index.d.ts","../../node_modules/.pnpm/zod@3.24.4/node_modules/zod/index.d.ts","../../packages/shared/src/constants/pagination.ts","../../packages/shared/src/constants/transactions.ts","../../packages/shared/src/validation/basevalidations.ts","../../packages/shared/src/types/types.ts","../../packages/shared/src/types/account.ts","../../packages/shared/src/types/user.ts","../../packages/shared/src/constants/currencies.ts","../../packages/shared/src/constants/operations.ts","../../packages/shared/src/constants/index.ts","../../packages/shared/src/validation/accounts.ts","../../packages/shared/src/validation/transactions.ts","../../packages/shared/src/validation/users.ts","../../packages/shared/src/validation/auth.ts","../../packages/shared/src/validation/commodities.ts","../../packages/shared/src/validation/index.ts","../../packages/shared/src/types/auth.ts","../../packages/shared/src/types/error-contract.ts","../../packages/shared/src/types/pagination.ts","../../packages/shared/src/types/transaction.ts","../../packages/shared/src/types/commodity.ts","../../packages/shared/src/types/index.ts","../../packages/shared/src/constants/accounts.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/types.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/locale/index.d.ts","../../node_modules/.pnpm/dayjs@1.11.13/node_modules/dayjs/index.d.ts","../../packages/shared/src/libs/formatters.ts","../../packages/shared/src/libs/index.ts","../../packages/shared/src/routes/index.ts","../../node_modules/.pnpm/@vitest+pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/types.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/helpers.d.ts","../../node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/index-8b61d5bc.d.ts","../../node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/index.d.ts","../../node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/tasks.d-cksck4of.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/types.d-bcelap-c.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/diff.d.ts","../../node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/types.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/error.d.ts","../../node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/index.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/optional-types.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/environment.d.cl3nlxbe.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.15.18/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+estree@1.0.7/node_modules/@types/estree/index.d.ts","../../node_modules/.pnpm/rollup@4.40.2/node_modules/rollup/dist/rollup.d.ts","../../node_modules/.pnpm/rollup@4.40.2/node_modules/rollup/dist/parseast.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/dist/node/modulerunnertransport.d-dj_me5sf.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/dist/node/module-runner.d.ts","../../node_modules/.pnpm/esbuild@0.25.4/node_modules/esbuild/lib/main.d.ts","../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/previous-map.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/input.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/css-syntax-error.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/declaration.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/root.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/warning.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/lazy-result.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/no-work-result.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/processor.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/result.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/document.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/rule.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/node.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/comment.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/container.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/at-rule.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/list.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/postcss.d.ts","../../node_modules/.pnpm/postcss@8.5.3/node_modules/postcss/lib/postcss.d.mts","../../node_modules/.pnpm/lightningcss@1.30.1/node_modules/lightningcss/node/ast.d.ts","../../node_modules/.pnpm/lightningcss@1.30.1/node_modules/lightningcss/node/targets.d.ts","../../node_modules/.pnpm/lightningcss@1.30.1/node_modules/lightningcss/node/index.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/internal/lightningcssoptions.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/internal/csspreprocessoroptions.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/types/metadata.d.ts","../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite/dist/node/index.d.ts","../../node_modules/.pnpm/@vitest+mocker@3.2.4_vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1_/node_modules/@vitest/mocker/dist/registry.d-d765pazg.d.ts","../../node_modules/.pnpm/@vitest+mocker@3.2.4_vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1_/node_modules/@vitest/mocker/dist/types.d-d_arzrdy.d.ts","../../node_modules/.pnpm/@vitest+mocker@3.2.4_vite@6.3.5_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1_/node_modules/@vitest/mocker/dist/index.d.ts","../../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/source-map.d.ts","../../node_modules/.pnpm/vite-node@3.2.4_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite-node/dist/trace-mapping.d-dlvdeqop.d.ts","../../node_modules/.pnpm/vite-node@3.2.4_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite-node/dist/index.d-dgmxd2u7.d.ts","../../node_modules/.pnpm/vite-node@3.2.4_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite-node/dist/index.d.ts","../../node_modules/.pnpm/@vitest+snapshot@3.2.4/node_modules/@vitest/snapshot/dist/environment.d-dhdq1csl.d.ts","../../node_modules/.pnpm/@vitest+snapshot@3.2.4/node_modules/@vitest/snapshot/dist/rawsnapshot.d-lfsmjfud.d.ts","../../node_modules/.pnpm/@vitest+snapshot@3.2.4/node_modules/@vitest/snapshot/dist/index.d.ts","../../node_modules/.pnpm/@vitest+snapshot@3.2.4/node_modules/@vitest/snapshot/dist/environment.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/config.d.d2roskhv.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/worker.d.1gmbbd7g.d.ts","../../node_modules/.pnpm/@types+deep-eql@4.0.2/node_modules/@types/deep-eql/index.d.ts","../../node_modules/.pnpm/@types+chai@5.2.2/node_modules/@types/chai/index.d.ts","../../node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/utils.d.ts","../../node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/benchmark.d.bwvbvtda.d.ts","../../node_modules/.pnpm/vite-node@3.2.4_@types+node@22.15.18_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vite-node/dist/client.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/coverage.d.s9rmnxie.d.ts","../../node_modules/.pnpm/@vitest+snapshot@3.2.4/node_modules/@vitest/snapshot/dist/manager.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/reporters.d.bflkqcl6.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/worker.d.ckwwzbsj.d.ts","../../node_modules/.pnpm/@vitest+spy@3.2.4/node_modules/@vitest/spy/dist/index.d.ts","../../node_modules/.pnpm/@vitest+expect@3.2.4/node_modules/@vitest/expect/dist/index.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/global.d.mamajcmj.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/vite.d.cmlllifp.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/mocker.d.be_2ls6u.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/chunks/suite.d.fvehnv49.d.ts","../../node_modules/.pnpm/expect-type@1.2.2/node_modules/expect-type/dist/utils.d.ts","../../node_modules/.pnpm/expect-type@1.2.2/node_modules/expect-type/dist/overloads.d.ts","../../node_modules/.pnpm/expect-type@1.2.2/node_modules/expect-type/dist/branding.d.ts","../../node_modules/.pnpm/expect-type@1.2.2/node_modules/expect-type/dist/messages.d.ts","../../node_modules/.pnpm/expect-type@1.2.2/node_modules/expect-type/dist/index.d.ts","../../node_modules/.pnpm/vitest@3.2.4_@types+node@22.15.18_@vitest+ui@3.2.4_jiti@2.4.2_lightningcss@1.30.1_tsx@4.19.4_yaml@2.8.1/node_modules/vitest/dist/index.d.ts","../../packages/shared/src/validation/accounts.test.ts","../../packages/shared/src/validation/basevalidations.test.ts","../../packages/shared/src/validation/transactions.test.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.4/node_modules/@types/react-dom/index.d.ts"],"fileIdsList":[[51,123,165],[50,51,52,53,60,123,165],[60,123,165],[123,165,262],[123,165],[123,162,165],[123,164,165],[165],[123,165,170,200],[123,165,166,171,177,178,185,197,208],[123,165,166,167,177,185],[118,119,120,123,165],[123,165,168,209],[123,165,169,170,178,186],[123,165,170,197,205],[123,165,171,173,177,185],[123,164,165,172],[123,165,173,174],[123,165,177],[123,165,175,177],[123,164,165,177],[123,165,177,178,179,197,208],[123,165,177,178,179,192,197,200],[123,160,165,213],[123,160,165,173,177,180,185,197,208],[123,165,177,178,180,181,185,197,205,208],[123,165,180,182,197,205,208],[121,122,123,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214],[123,165,177,183],[123,165,184,208],[123,165,173,177,185,197],[123,165,186],[123,165,187],[123,164,165,188],[123,162,163,164,165,166,167,168,169,170,171,172,173,174,175,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214],[123,165,190],[123,165,191],[123,165,177,192,193],[123,165,192,194,209,211],[123,165,177,197,198,200],[123,165,199,200],[123,165,197,198],[123,165,200],[123,165,201],[123,162,165,197],[123,165,177,203,204],[123,165,203,204],[123,165,170,185,197,205],[123,165,206],[123,165,185,207],[123,165,180,191,208],[123,165,170,209],[123,165,197,210],[123,165,184,211],[123,165,212],[123,165,170,177,179,188,197,208,211,213],[123,165,197,214],[50,123,165],[48,49,123,165],[108,109,112,123,165,272],[123,165,249,250],[109,110,112,113,114,123,165],[109,123,165],[109,110,112,123,165],[109,110,123,165],[123,165,256],[104,123,165,256,257],[104,123,165,256],[104,111,123,165],[105,123,165],[104,105,106,108,123,165],[104,123,165],[99,123,165],[98,123,165],[123,165,278,279],[123,165,278,279,280,281],[123,165,278,280],[123,165,278],[123,165,242,243],[123,165,237],[123,165,235,237],[123,165,226,234,235,236,238],[123,165,224],[123,165,227,232,237,240],[123,165,223,240],[123,165,227,228,231,232,233,240],[123,165,227,228,229,231,232,240],[123,165,224,225,226,227,228,232,233,234,236,237,238,240],[123,165,240],[123,165,222,224,225,226,227,228,229,231,232,233,234,235,236,237,238,239],[123,165,222,240],[123,165,227,229,230,232,233,240],[123,165,231,240],[123,165,232,233,237,240],[123,165,225,235],[123,165,217,247,248],[123,165,216,217],[107,123,165],[123,132,136,165,208],[123,132,165,197,208],[123,127,165],[123,129,132,165,205,208],[123,165,185,205],[123,165,215],[123,127,165,215],[123,129,132,165,185,208],[123,124,125,128,131,165,177,197,208],[123,132,139,165],[123,124,130,165],[123,132,153,154,165],[123,128,132,165,200,208,215],[123,153,165,215],[123,126,127,165,215],[123,132,165],[123,126,127,128,129,130,131,132,133,134,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,154,155,156,157,158,159,165],[123,132,147,165],[123,132,139,140,165],[123,130,132,140,141,165],[123,131,165],[123,124,127,132,165],[123,132,136,140,141,165],[123,136,165],[123,130,132,135,165,208],[123,124,129,132,139,165],[123,165,197],[123,127,132,153,165,213,215],[123,165,253,254],[123,165,253],[59,123,165],[55,56,58,123,165,177,178,180,181,182,185,197,205,208,214,215,217,218,219,220,221,241,245,246,247,248],[55,56,57,123,165,219],[55,123,165],[56,123,165],[57,58,123,165],[123,165,244],[123,165,217,248],[115,123,165,264,265,274],[104,112,115,123,165,258,259,274],[123,165,267],[116,123,165],[104,115,117,123,165,258,266,273,274],[123,165,251],[104,109,112,115,117,123,165,168,178,197,248,251,252,255,258,260,261,263,266,268,269,274,275],[115,123,165,264,265,266,274],[123,165,248,270,275],[115,117,123,165,255,258,260,274],[123,165,213,261],[104,109,112,115,116,117,123,165,168,178,197,213,248,251,252,255,258,259,260,261,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,282],[74,123,165],[64,65,123,165],[62,63,64,66,67,72,123,165],[63,64,123,165],[73,123,165],[64,123,165],[62,63,64,67,68,69,70,71,123,165],[62,63,74,123,165],[96,123,165],[76,77,82,83,97,123,165],[76,123,165],[100,123,165],[101,123,165],[79,123,165],[75,90,123,165],[79,80,81,91,92,93,94,95,123,165],[75,78,79,93,123,165],[75,78,123,165],[85,123,165,283],[75,78,84,123,165],[75,123,165],[78,123,165,283],[75,76,77,123,165],[78,85,86,87,88,89,123,165],[86,123,165,283],[75,77,78,123,165]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"742d4b7b02ffc3ba3c4258a3d196457da2b3fec0125872fd0776c50302a11b9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"03566a51ebc848dec449a4ed69518e9f20caa6ac123fa32676aaaabe64adae8e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"9376f28e07595fa672e0dc353d78cae77d8df36d6a1652f2e65980bb8a18269c","signature":"4a02a6e3d810cdea6609e913c5177d935862a694c961c0d0807fee153598d211"},{"version":"88e9caa9c5d2ba629240b5913842e7c57c5c0315383b8dc9d436ef2b60f1c391","impliedFormat":1},{"version":"76925507beff62d7fe6794d554a86a45823bc0bd109a01eda4aaea2b5796b4fd","signature":"5e27e58e235a551ef56cb47dbd7c7f4686e88886a9cf1d8b6226833f84013fff"},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"19efad8495a7a6b064483fccd1d2b427403dd84e67819f86d1c6ee3d7abf749c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eef826bc4a19de22155487984e345a34c9cd511dd1170edc7a447cb8231dd4a","affectsGlobalScope":true,"impliedFormat":99},{"version":"6bfdbd285cb25907f31ba9db446e61eb2aa55cf46220ebe5fceaa18b2a0b26e8","affectsGlobalScope":true},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"950f2cd81e30d0ecdf70ab78fcfd85fc5bb28b45ebb08c860daff059feea412e","impliedFormat":1},{"version":"3a5af4fba7b27b815bb40f52715aedebaa4b371da3e5a664e7e0798c9b638825","impliedFormat":1},{"version":"8485b6da53ec35637d072e516631d25dae53984500de70a6989058f24354666f","impliedFormat":1},{"version":"ebe80346928736532e4a822154eb77f57ef3389dbe2b3ba4e571366a15448ef2","impliedFormat":1},{"version":"49c632082dc8a916353288d3d8b2dc82b3471794249a381d090d960c8ceac908","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"71addb585c2db7b8e53dc1b0bcfa58c6c67c6e4fa2b968942046749d66f82e7e","impliedFormat":1},{"version":"c76b0c5727302341d0bdfa2cc2cee4b19ff185b554edb6e8543f0661d8487116","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"e703cfacb9965c4d4155346c65a0091ecded90ea98874ed6b3f36286577c4dde","impliedFormat":1},{"version":"f5ef066942e4f0bd98200aa6a6694b831e73200c9b3ade77ad0aa2409e8fe1b1","impliedFormat":1},{"version":"b9e99cd94f4166a245f5158f7286c05406e2a4c694619bceb7a4f3519d1d768e","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"9563572d81b4aa8b7cf3e24384262341bd53a7a1e5e55034f5d446654b824e8a","signature":"bce0cc5be1903519d3a629f49137b213c22e1d4380a6b4cd409fe5bc38a7dd09"},{"version":"10e44a443fcc02629276b3068a04090ef67456c7b9f49a767e566575625852b7","signature":"2966ffb8b17167488deb293aaa49f5da898066d32ce422afbbf98fc516a42fc1"},{"version":"b16588ab6baa1e644b52a61a7aab09944b97d551e63d41737511ca0fdaa48240","signature":"7de13694dfd164dafb704864c2b605059ca1b9da2ecce783a52640b0601d0deb"},{"version":"588729217e9d3bdb0b58d99e9c53a26d5910f1528581fc6e382be3be503224f3","signature":"cee9bde02183941aad2a2703006d188a784d74c6125a1a6e761c22fc8e4ee605"},{"version":"6a10203ade5f669e7d050ada19875acffd324212115228bc8725949a22cc76bf","signature":"77b59af0be64b7a6b99016d2f5c4398c94f4c40744b12dd5cd61f966b14c4c76"},{"version":"5c043033f251a8c5bd7108914393af575650fa9c78a78e548f81d7cfc4014e6f","signature":"74555895068502b65799c3c9928e42f4e56d6f69b021e6f13f294c1a8c354260"},{"version":"5275b12537eadeeac78e852fd6ddebb4882770947c64f06bf1bdaf29ecb61303","signature":"c4a90e38375dfd22ab91d9a420cfa703a5dca4145108e0d1106e9873bc96797b"},{"version":"1af38648bd7056b360f5c3607b305f558a09d1d320fa253a7333c169ea9529b1","signature":"08f9b11447950c4f59141f328520c6e31b5a166924336777995a0187f7a4bde9"},"9b1f5337bffec91b0d8559a08d004f2f35357247a6581f8e0650200f344aa1de",{"version":"b08fcbc6e69b649f9a551c8e3b037745297dafcb5e9b2ff535ba68094391828d","signature":"4ff06802f14e5bbb4a23ef7b00c856778b62cc011f06c99b72245ab8ad84010b"},{"version":"8e739002a2c44490f6dfb15ef1d9a064f00995a60a548003520da2c258de2daf","signature":"65f000f52bfedacb31a6db1f6d73da660498679396c5c524e38581922978be44"},{"version":"475aecf30db5bec8a62e0ffd4b9924cd8693b814019332a9bc3c4853d12f7657","signature":"a6785ff8e286cc4b64221c4d9c6a1084f328837d1e3225db9045130dcc8a0ca8"},{"version":"c4a595c3f6c279163c73fde607c056c813ef53f47537bc91b2623dd95f45f2fc","signature":"0dacf73b071d41f5506c1858dbc30a73b85482307cb5e15a63ba4fe654906ce4"},{"version":"ed3e852ca411e14e13830f6a4dfd400148a288bc3ae0ad491e8271e6f48dd762","signature":"8a213784ba67fbef718313cf101abea9344ecda14d2bb6bff94f865c03078234"},"297345b01475c22e441a079350ad6a0eaac4a20bf494e609c2887dc84987bdcc",{"version":"3b96187464c836feb9457f746b1ed44dc91ad76aa65ee8cdd64283b46fda9234","signature":"ec7adc98a93755b7c1ecb861e78423a549a0d8b30751571340bac9c0c974c66a"},{"version":"b4b0f493c3ebf51fc982a1ce59e95b863836dc00c5685d09289cf1953a40fba0","signature":"db39cdaf4c7f6939a6e0533c49464ad9d9a7699adb355247500f92cce14d2a67"},{"version":"b4508ea58d02c3b0931995c9d5a5f464864efa7242243677de743ff9d248ab0d","signature":"02b514f6c19f6e26cd1993dd2504a4d94566ea6af541e90dfcc8ed378c1f0eac"},{"version":"0b4b350c8213fa00346cd95d2ecd342abe0e715b631441517b9c9b8fed415ffd","signature":"aebcd9d3d659ae67d056b135e8106bebe305147634c712f006cf4f18f98e55ed"},{"version":"c006648fc51b28eb665766dad55b4c29216d8ef0c68c2c126d68f12210020085","signature":"4510a663c34911f93bd529151c52ebc9c4d419d1698a6d76d0e945bd4f0ef43d"},"c8ee5252beca2d319a6278420d47c729480b35fd410a970fb96f5bdf925cb527",{"version":"b9b5488f9e8043ff017f648745a75b1c2e030a8efa16c6310fa6e6f9e52bcef5","signature":"58969267c23c27443642dec465082e37d7e210fa71c2cf13f516d876ff97b6fa"},{"version":"73a0ee6395819b063df4b148211985f2e1442945c1a057204cf4cf6281760dc3","affectsGlobalScope":true,"impliedFormat":1},{"version":"d05d8c67116dceafc62e691c47ac89f8f10cf7313cd1b2fb4fe801c2bf1bb1a7","impliedFormat":1},{"version":"3c5bb5207df7095882400323d692957e90ec17323ccff5fd5f29a1ecf3b165d0","impliedFormat":1},{"version":"7959556e748fee75a1ed10822bed4e92edae99ed75e428b650376bacb20ce257","signature":"5a81de12417101e8964d1370944563204a470760be39079613c910b549a0b53a"},"4ca18ab8cd76fb5fb9f7013772f05cdf7b17cdd2f65c39a20399c01c04952a9e",{"version":"4c3ca267ee80eeaf80286cc85d7ce0002d816762421ea30bacfa4251cb3cb3ba","signature":"d0693ad605cf280272587f426caee0f16c0f008dd2cc0687ee6a69effc0fe05f"},{"version":"5c54a34e3d91727f7ae840bfe4d5d1c9a2f93c54cb7b6063d06ee4a6c3322656","impliedFormat":99},{"version":"db4da53b03596668cf6cc9484834e5de3833b9e7e64620cf08399fe069cd398d","impliedFormat":99},{"version":"ac7c28f153820c10850457994db1462d8c8e462f253b828ad942a979f726f2f9","impliedFormat":99},{"version":"f9b028d3c3891dd817e24d53102132b8f696269309605e6ed4f0db2c113bbd82","impliedFormat":99},{"version":"fb7c8d90e52e2884509166f96f3d591020c7b7977ab473b746954b0c8d100960","impliedFormat":99},{"version":"0bff51d6ed0c9093f6955b9d8258ce152ddb273359d50a897d8baabcb34de2c4","impliedFormat":99},{"version":"45cec9a1ba6549060552eead8959d47226048e0b71c7d0702ae58b7e16a28912","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"13918e2b81c4288695f9b1f3dcc2468caf0f848d5c1f3dc00071c619d34ff63a","impliedFormat":99},{"version":"6907b09850f86610e7a528348c15484c1e1c09a18a9c1e98861399dfe4b18b46","impliedFormat":99},{"version":"12deea8eaa7a4fc1a2908e67da99831e5c5a6b46ad4f4f948fd4759314ea2b80","impliedFormat":99},{"version":"f0a8b376568a18f9a4976ecb0855187672b16b96c4df1c183a7e52dc1b5d98e8","impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"092944a8c05f9b96579161e88c6f211d5304a76bd2c47f8d4c30053269146bc8","impliedFormat":99},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"3b724a66c071d616203133f8d099a0cb881b0b43fd42e8621e611243c5f30cd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a38efe83ff77c34e0f418a806a01ca3910c02ee7d64212a59d59bca6c2c38fa1","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"3fe4022ba1e738034e38ad9afacbf0f1f16b458ed516326f5bf9e4a31e9be1dc","impliedFormat":1},{"version":"a957197054b074bcdf5555d26286e8461680c7c878040d0f4e2d5509a7524944","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"e9b97d69510658d2f4199b7d384326b7c4053b9e6645f5c19e1c2a54ede427fc","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"f478f6f5902dc144c0d6d7bdc919c5177cac4d17a8ca8653c2daf6d7dc94317f","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"b200675fd112ffef97c166d0341fb33f6e29e9f27660adde7868e95c5bc98beb","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a0a1dda070290b92da5a50113b73ecc4dd6bcbffad66e3c86503d483eafbadcf","impliedFormat":1},{"version":"59dcad36c4549175a25998f6a8b33c1df8e18df9c12ebad1dfb25af13fd4b1ce","impliedFormat":1},{"version":"9ba5b6a30cb7961b68ad4fb18dca148db151c2c23b8d0a260fc18b83399d19d3","impliedFormat":1},{"version":"3f3edb8e44e3b9df3b7ca3219ab539710b6a7f4fe16bd884d441af207e03cd57","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"d71535813e39c23baa113bc4a29a0e187b87d1105ccc8c5a6ebaca38d9a9bff2","impliedFormat":1},{"version":"8cf7e92bdb2862c2d28ba4535c43dc599cfbc0025db5ed9973d9b708dcbe3d98","affectsGlobalScope":true,"impliedFormat":1},{"version":"278e70975bd456bba5874eaee17692355432e8d379b809a97f6af0eee2b702d8","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"636302a00dfd1f9fe6e8e91e4e9350c6518dcc8d51a474e4fc3a9ba07135100b","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"e1ce1d622f1e561f6cdf246372ead3bbc07ce0342024d0e9c7caf3136f712698","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"27e4532aaaa1665d0dd19023321e4dc12a35a741d6b8e1ca3517fcc2544e0efe","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"8c2ad42d5d1a2e8e6112625767f8794d9537f1247907378543106f7ba6c7df90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"98ffdf93dfdd206516971d28e3e473f417a5cfd41172e46b4ce45008f640588e","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"12e8ce658dd17662d82fb0509d2057afc5e6ee30369a2e9e0957eff725b1f11d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74736930d108365d7bbe740c7154706ccfb1b2a3855a897963ab3e5c07ecbf19","impliedFormat":1},{"version":"858f999b3e4a45a4e74766d43030941466460bf8768361d254234d5870480a53","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"63b05afa6121657f25e99e1519596b0826cda026f09372c9100dfe21417f4bd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"3797dd6f4ea3dc15f356f8cdd3128bfa18122213b38a80d6c1f05d8e13cbdad8","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1},{"version":"e2b48abff5a8adc6bb1cd13a702b9ef05e6045a98e7cfa95a8779b53b6d0e69d","impliedFormat":1},{"version":"a02d26c056491b1ddfa53a671ad60ce852969b369f0e71993dbac8ddcf0d038b","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"035d0934d304483f07148427a5bd5b98ac265dae914a6b49749fe23fbd893ec7","impliedFormat":99},{"version":"e2ed5b81cbed3a511b21a18ab2539e79ac1f4bc1d1d28f8d35d8104caa3b429f","impliedFormat":99},{"version":"dd7ca4f0ef3661dac7043fb2cdf1b99e008d2b6bc5cd998dd1fa5a2968034984","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"33f3718dababfc26dfd9832c150149ea4e934f255130f8c118a59ae69e5ed441","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1bb61aa2f08ab4506d41dbe16c5f3f5010f014bbf46fa3d715c0cbe3b00f4e1c","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"e42820cd611b15910c204cd133f692dcd602532b39317d4f2a19389b27e6f03d","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"f7eebe1b25040d805aefe8971310b805cd49b8602ec206d25b38dc48c542f165","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"509f8efdfc5f9f6b52284170e8d7413552f02d79518d1db691ee15acc0088676","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"7870becb94cbc11d2d01b77c4422589adcba4d8e59f726246d40cd0d129784d8","affectsGlobalScope":true,"impliedFormat":1},{"version":"f70b8328a15ca1d10b1436b691e134a49bc30dcf3183a69bfaa7ba77e1b78ecd","impliedFormat":1},{"version":"ff3660e2664e6096196280deb4e176633b1bb1e58a7dcc9b021ec0e913a6f96f","impliedFormat":99},{"version":"b34b5f6b506abb206b1ea73c6a332b9ee9c8c98be0f6d17cdbda9430ecc1efab","impliedFormat":99},{"version":"75d4c746c3d16af0df61e7b0afe9606475a23335d9f34fcc525d388c21e9058b","impliedFormat":99},{"version":"fa959bf357232201c32566f45d97e70538c75a093c940af594865d12f31d4912","impliedFormat":99},{"version":"d2c52abd76259fc39a30dfae70a2e5ce77fd23144457a7ff1b64b03de6e3aec7","impliedFormat":99},{"version":"e6233e1c976265e85aa8ad76c3881febe6264cb06ae3136f0257e1eab4a6cc5a","impliedFormat":99},{"version":"f73e2335e568014e279927321770da6fe26facd4ac96cdc22a56687f1ecbb58e","impliedFormat":99},{"version":"317878f156f976d487e21fd1d58ad0461ee0a09185d5b0a43eedf2a56eb7e4ea","impliedFormat":99},{"version":"324ac98294dab54fbd580c7d0e707d94506d7b2c3d5efe981a8495f02cf9ad96","impliedFormat":99},{"version":"9ec72eb493ff209b470467e24264116b6a8616484bca438091433a545dfba17e","impliedFormat":99},{"version":"d6ee22aba183d5fc0c7b8617f77ee82ecadc2c14359cc51271c135e23f6ed51f","impliedFormat":99},{"version":"49747416f08b3ba50500a215e7a55d75268b84e31e896a40313c8053e8dec908","impliedFormat":99},{"version":"81e634f1c5e1ca309e7e3dc69e2732eea932ef07b8b34517d452e5a3e9a36fa3","impliedFormat":99},{"version":"34f39f75f2b5aa9c84a9f8157abbf8322e6831430e402badeaf58dd284f9b9a6","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"c8905dbea83f3220676a669366cd8c1acef56af4d9d72a8b2241b1d044bb4302","affectsGlobalScope":true,"impliedFormat":99},{"version":"891694d3694abd66f0b8872997b85fd8e52bc51632ce0f8128c96962b443189f","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"971a2c327ff166c770c5fb35699575ba2d13bba1f6d2757309c9be4b30036c8e","impliedFormat":99},{"version":"4f45e8effab83434a78d17123b01124259fbd1e335732135c213955d85222234","impliedFormat":99},{"version":"7bd51996fb7717941cbe094b05adc0d80b9503b350a77b789bbb0fc786f28053","impliedFormat":99},{"version":"b62006bbc815fe8190c7aee262aad6bff993e3f9ade70d7057dfceab6de79d2f","impliedFormat":99},{"version":"13497c0d73306e27f70634c424cd2f3b472187164f36140b504b3756b0ff476d","impliedFormat":99},{"version":"bf7a2d0f6d9e72d59044079d61000c38da50328ccdff28c47528a1a139c610ec","impliedFormat":99},{"version":"04471dc55f802c29791cc75edda8c4dd2a121f71c2401059da61eff83099e8ab","impliedFormat":99},{"version":"120a80aa556732f684db3ed61aeff1d6671e1655bd6cba0aa88b22b88ac9a6b1","affectsGlobalScope":true,"impliedFormat":99},{"version":"e58c0b5226aff07b63be6ac6e1bec9d55bc3d2bda3b11b9b68cccea8c24ae839","affectsGlobalScope":true,"impliedFormat":99},{"version":"a23a08b626aa4d4a1924957bd8c4d38a7ffc032e21407bbd2c97413e1d8c3dbd","impliedFormat":99},{"version":"5a88655bf852c8cc007d6bc874ab61d1d63fba97063020458177173c454e9b4a","impliedFormat":99},{"version":"7e4dfae2da12ec71ffd9f55f4641a6e05610ce0d6784838659490e259e4eb13c","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"4c8ca51077f382498f47074cf304d654aba5d362416d4f809dfdd5d4f6b3aaca","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"13573a613314e40482386fe9c7934f9d86f3e06f19b840466c75391fb833b99b","impliedFormat":99},{"version":"d64dfcaf017e7f655369c96fdceb0263988f744f5993dd3a6d5d8ceba3270467","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"776a79ff22af80f6311f47ec6dc535954f9812b2802d9022aca5b805cb1839e1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fd2acf9e3fbe9231047047071e8298e040e0186599416658900762e2483f5c8a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a0acca63c9e39580f32a10945df231815f0fe554c074da96ba6564010ffbd2d8","impliedFormat":1}],"root":[52,54,61,[76,97],[101,103],[284,286]],"options":{"allowImportingTsExtensions":true,"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"esModuleInterop":true,"jsx":4,"module":99,"noFallthroughCasesInSwitch":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"strict":true,"target":7,"useDefineForClassFields":true},"referencedMap":[[52,1],[54,2],[61,3],[263,4],[262,5],[216,5],[162,6],[163,6],[164,7],[123,8],[165,9],[166,10],[167,11],[118,5],[121,12],[119,5],[120,5],[168,13],[169,14],[170,15],[171,16],[172,17],[173,18],[174,18],[176,19],[175,20],[177,21],[178,22],[179,23],[161,24],[122,5],[180,25],[181,26],[182,27],[215,28],[183,29],[184,30],[185,31],[186,32],[187,33],[188,34],[189,35],[190,36],[191,37],[192,38],[193,38],[194,39],[195,5],[196,5],[197,40],[199,41],[198,42],[200,43],[201,44],[202,45],[203,46],[204,47],[205,48],[206,49],[207,50],[208,51],[209,52],[210,53],[211,54],[212,55],[213,56],[214,57],[53,58],[287,58],[48,5],[50,59],[51,58],[273,60],[251,61],[249,5],[250,5],[104,5],[115,62],[110,63],[113,64],[264,65],[256,5],[259,66],[258,67],[269,67],[257,68],[272,5],[112,69],[114,69],[106,70],[109,71],[252,70],[111,72],[105,5],[49,5],[100,73],[99,74],[98,5],[221,5],[280,75],[282,76],[281,77],[279,78],[278,5],[242,5],[244,79],[243,5],[238,80],[236,81],[237,82],[225,83],[226,81],[233,84],[224,85],[229,86],[239,5],[230,87],[235,88],[241,89],[240,90],[223,91],[231,92],[232,93],[227,94],[234,80],[228,95],[218,96],[217,97],[222,5],[265,5],[107,5],[108,98],[46,5],[47,5],[8,5],[9,5],[11,5],[10,5],[2,5],[12,5],[13,5],[14,5],[15,5],[16,5],[17,5],[18,5],[19,5],[3,5],[20,5],[21,5],[4,5],[22,5],[26,5],[23,5],[24,5],[25,5],[27,5],[28,5],[29,5],[5,5],[30,5],[31,5],[32,5],[33,5],[6,5],[37,5],[34,5],[35,5],[36,5],[38,5],[7,5],[39,5],[44,5],[45,5],[40,5],[41,5],[42,5],[43,5],[1,5],[139,99],[149,100],[138,99],[159,101],[130,102],[129,103],[158,104],[152,105],[157,106],[132,107],[146,108],[131,109],[155,110],[127,111],[126,104],[156,112],[128,113],[133,114],[134,5],[137,114],[124,5],[160,115],[150,116],[141,117],[142,118],[144,119],[140,120],[143,121],[153,104],[135,122],[136,123],[145,124],[125,125],[148,116],[147,114],[151,5],[154,126],[267,127],[254,128],[255,127],[253,5],[60,129],[248,130],[220,131],[219,132],[56,132],[55,5],[57,133],[58,5],[59,134],[246,5],[245,135],[247,136],[266,137],[260,138],[268,139],[117,140],[274,141],[276,142],[270,143],[277,144],[275,145],[261,146],[271,147],[283,148],[116,5],[75,149],[66,150],[73,151],[68,5],[69,5],[67,152],[70,149],[62,5],[63,5],[74,153],[65,154],[71,5],[72,155],[64,156],[97,157],[82,5],[84,158],[83,5],[76,5],[77,159],[101,160],[102,161],[103,5],[80,162],[91,163],[95,162],[92,162],[96,164],[93,5],[94,165],[79,166],[81,162],[284,167],[85,168],[88,169],[285,170],[78,171],[89,166],[90,172],[286,173],[86,174],[87,166]],"affectedFilesPendingEmit":[[52,17],[54,17],[97,17],[82,17],[84,17],[83,17],[76,17],[77,17],[101,17],[102,17],[103,17],[80,17],[91,17],[95,17],[92,17],[96,17],[93,17],[94,17],[79,17],[81,17],[284,17],[85,17],[88,17],[285,17],[78,17],[89,17],[90,17],[286,17],[86,17],[87,17]],"emitSignatures":[52,54,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,101,102,103,284,285,286],"version":"5.8.3"} \ No newline at end of file diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 538bd9f1..5c4005a7 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -1,7 +1,5 @@ import path from 'path'; -import tailwindcss from '@tailwindcss/vite'; -import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; import react from '@vitejs/plugin-react'; import { defineConfig, loadEnv } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; @@ -22,12 +20,7 @@ export default defineConfig(({ mode }) => { outDir: 'dist', }, envDir: path.resolve(__dirname, '../..'), - plugins: [ - TanStackRouterVite({ autoCodeSplitting: true, target: 'react' }), - react(), - tailwindcss(), - tsconfigPaths(), - ], + plugins: [react(), tsconfigPaths()], server: { host, open: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 244c1885..4e2c6324 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,55 +130,13 @@ importers: apps/frontend: dependencies: - '@hookform/resolvers': - specifier: ^5.0.1 - version: 5.0.1(react-hook-form@7.56.4(react@19.1.0)) - '@ledgerly/shared': - specifier: workspace:* - version: link:../../packages/shared - '@tailwindcss/vite': - specifier: ^4.1.5 - version: 4.1.7(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1)) - '@tanstack/react-router': - specifier: ^1.119.0 - version: 1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/react-router-devtools': - specifier: ^1.119.1 - version: 1.120.5(@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.120.5)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3) - clsx: - specifier: ^2.1.1 - version: 2.1.1 - lucide-react: - specifier: ^0.507.0 - version: 0.507.0(react@19.1.0) - mobx: - specifier: ^6.13.7 - version: 6.13.7 - mobx-react-lite: - specifier: ^4.1.0 - version: 4.1.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) - react-hook-form: - specifier: ^7.56.2 - version: 7.56.4(react@19.1.0) - tailwind-merge: - specifier: ^3.3.0 - version: 3.3.0 - tailwindcss: - specifier: ^4.1.5 - version: 4.1.7 devDependencies: - '@tanstack/eslint-plugin-router': - specifier: ^1.115.0 - version: 1.115.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@tanstack/router-plugin': - specifier: ^1.119.0 - version: 1.120.5(@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1)) '@types/react': specifier: ^19.0.10 version: 19.1.4 @@ -188,9 +146,6 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1)) - daisyui: - specifier: ^5.0.35 - version: 5.0.35 eslint-plugin-react: specifier: ^7.37.5 version: 7.37.5(eslint@9.27.0(jiti@2.4.2)) @@ -280,18 +235,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -679,11 +622,6 @@ packages: '@fastify/proxy-addr@5.0.0': resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} - '@hookform/resolvers@5.0.1': - resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} - peerDependencies: - react-hook-form: ^7.55.0 - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -708,10 +646,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -978,186 +912,6 @@ packages: '@rushstack/ts-command-line@5.0.1': resolution: {integrity: sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==} - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - - '@tailwindcss/node@4.1.7': - resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} - - '@tailwindcss/oxide-android-arm64@4.1.7': - resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.1.7': - resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.1.7': - resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.1.7': - resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': - resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': - resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': - resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': - resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.1.7': - resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-wasm32-wasi@4.1.7': - resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': - resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': - resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.1.7': - resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} - engines: {node: '>= 10'} - - '@tailwindcss/vite@4.1.7': - resolution: {integrity: sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==} - peerDependencies: - vite: ^5.2.0 || ^6 - - '@tanstack/eslint-plugin-router@1.115.0': - resolution: {integrity: sha512-vwKP9NjzAQ2nU3qADDoTSTxD1vNduLcK0312AWkXkicCWUZQZJtmRuZXPk9cEfuzDD2BAJCIUmlaF1K0vq4oRw==} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - - '@tanstack/history@1.115.0': - resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==} - engines: {node: '>=12'} - - '@tanstack/react-router-devtools@1.120.5': - resolution: {integrity: sha512-unCiEL/QbB4O2Gt5U5U6xPoo2fdA38J+1cAzv0CNiK3cuj+XNd/5oegIuusFn4z0W+S2CIxCBXR1w0A49iMxrA==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/react-router': ^1.120.5 - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-router@1.120.5': - resolution: {integrity: sha512-A+YRftGwAeFBxa8DF5ujNYqkSEbjCa1KjxDNYr+jWj16jjTxrz/XqgOJCv5ZfbAqqqOa3yLYoQbWa7OGz5jHuA==} - engines: {node: '>=12'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-store@0.7.0': - resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@tanstack/router-core@1.120.5': - resolution: {integrity: sha512-IXLNv3j7rpTL/YNCWHijZgrnxFuvD4Nz/nUiGSak4x5BKzlnuZEso81xFcIuczVrEW72NxZv8IfzpR5M5Tuc0A==} - engines: {node: '>=12'} - - '@tanstack/router-devtools-core@1.120.5': - resolution: {integrity: sha512-PciNudzA26JoE+M1LC2iBXZNOx2X2xCnccSdRLieuv6ZJrf+jT+rTYf59n/Z13H2oIJac1ZDv3oIOwLvfPCqkQ==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/router-core': ^1.120.5 - csstype: ^3.0.10 - solid-js: '>=1.9.5' - tiny-invariant: ^1.3.3 - peerDependenciesMeta: - csstype: - optional: true - - '@tanstack/router-generator@1.120.5': - resolution: {integrity: sha512-09YWjEXqrJE+j89gKBGSbt0ca3CYwo0QUlsDlCQ1WSI4e3GuQLvSROjq97nMjaPtrnWiROxgnw5S18BlcVVvfA==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/react-router': ^1.120.5 - peerDependenciesMeta: - '@tanstack/react-router': - optional: true - - '@tanstack/router-plugin@1.120.5': - resolution: {integrity: sha512-5w6vW7g5LgGZ1IM1D2HPInRmJHqK5jSa2REr7QFwKVDvbVHfzOAcqfAIw1kiPRRqIh/r6YSsrGyMV5MVbrJSCA==} - engines: {node: '>=12'} - peerDependencies: - '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.120.5 - vite: '>=5.0.0 || >=6.0.0' - vite-plugin-solid: ^2.11.2 - webpack: '>=5.92.0' - peerDependenciesMeta: - '@rsbuild/core': - optional: true - '@tanstack/react-router': - optional: true - vite: - optional: true - vite-plugin-solid: - optional: true - webpack: - optional: true - - '@tanstack/router-utils@1.115.0': - resolution: {integrity: sha512-Dng4y+uLR9b5zPGg7dHReHOTHQa6x+G6nCoZshsDtWrYsrdCcJEtLyhwZ5wG8OyYS6dVr/Cn+E5Bd2b6BhJ89w==} - engines: {node: '>=12'} - - '@tanstack/store@0.7.0': - resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} - - '@tanstack/virtual-file-routes@1.115.0': - resolution: {integrity: sha512-XLUh1Py3AftcERrxkxC5Y5m5mfllRH3YR6YVlyjFgI2Tc2Ssy2NKmQFQIafoxfW459UJ8Dn81nWKETEIJifE4g==} - engines: {node: '>=12'} - '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1417,10 +1171,6 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - ansis@3.17.0: - resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} - engines: {node: '>=14'} - anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1491,9 +1241,6 @@ packages: avvio@9.1.0: resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} - babel-dead-code-elimination@1.0.10: - resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1565,10 +1312,6 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1577,10 +1320,6 @@ packages: resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1624,9 +1363,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - daisyui@5.0.35: - resolution: {integrity: sha512-AWi11n/x5++mps55jcwrBf0Lmip1euWY0FYcH/05SFGmoqrU7S7/aIUWaiaeqlJ5EcmEZ/7zEY73aOxMv6hcIg==} - data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -1697,10 +1433,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - diff@7.0.0: - resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} - engines: {node: '>=0.3.1'} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1833,10 +1565,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} - engines: {node: '>=10.13.0'} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2237,11 +1965,6 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - goober@2.1.16: - resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} - peerDependencies: - csstype: ^3.0.10 - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2531,6 +2254,7 @@ packages: libsql@0.5.10: resolution: {integrity: sha512-lQu5RLqDLFuo6H5SuR3CnEstGVph77Jd8lm3fdW64p6tUjOC0X8Z9PlfAdZYxWyirYLH9uwcEZUrABsNKYwOiQ==} + cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] light-my-request@6.6.0: @@ -2644,11 +2368,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.507.0: - resolution: {integrity: sha512-XfgE6gvAHwAtnbUvWiTTHx4S3VGR+cUJHEc0vrh9Ogu672I1Tue2+Cp/8JJqpytgcBHAB1FVI297W4XGNwc2dQ==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2698,42 +2417,17 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} mnemonist@0.40.3: resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} - mobx-react-lite@4.1.0: - resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==} - peerDependencies: - mobx: ^6.9.0 - react: ^16.8.0 || ^17 || ^18 || ^19 - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - - mobx@6.13.7: - resolution: {integrity: sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -2957,12 +2651,6 @@ packages: peerDependencies: react: ^19.1.0 - react-hook-form@7.56.4: - resolution: {integrity: sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 || ^19 - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3083,16 +2771,6 @@ packages: engines: {node: '>=10'} hasBin: true - seroval-plugins@1.3.1: - resolution: {integrity: sha512-dOlUoiI3fgZbQIcj6By+l865pzeWdP3XCSLdI3xlKnjCk5983yLWPsXytFOUI0BUZKG9qwqbj78n9yVcVwUqaQ==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - - seroval@1.3.1: - resolution: {integrity: sha512-F+T9EQPdLzgdewgxnBh4mSc+vde+EOkU6dC9BDuu/bfGb+UyUlqM6t8znFCTPQSuai/ZcfFg0gu79h+bVW2O0w==} - engines: {node: '>=10'} - set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -3147,9 +2825,6 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} - solid-js@1.9.7: - resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} - sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -3258,20 +2933,6 @@ packages: resolution: {integrity: sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==} engines: {node: ^14.18.0 || >=16.0.0} - tailwind-merge@3.3.0: - resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} - - tailwindcss@4.1.7: - resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} - - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -3279,12 +2940,6 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tiny-warning@1.0.3: - resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3430,10 +3085,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unplugin@2.3.4: - resolution: {integrity: sha512-m4PjxTurwpWfpMomp8AptjD5yj8qEZN5uQjjGM3TAs9MWWD2tXSSNNj6jGR2FoVGod4293ytyV6SwBbertfyJg==} - engines: {node: '>=18.12.0'} - update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -3443,11 +3094,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -3548,9 +3194,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3618,10 +3261,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml@2.8.1: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} @@ -3722,16 +3361,6 @@ snapshots: dependencies: '@babel/types': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)': - dependencies: - '@babel/core': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.1)': - dependencies: - '@babel/core': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -4004,11 +3633,6 @@ snapshots: '@fastify/forwarded': 3.0.0 ipaddr.js: 2.2.0 - '@hookform/resolvers@5.0.1(react-hook-form@7.56.4(react@19.1.0))': - dependencies: - '@standard-schema/utils': 0.3.0 - react-hook-form: 7.56.4(react@19.1.0) - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4031,10 +3655,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@istanbuljs/schema@0.1.3': {} '@jridgewell/gen-mapping@0.3.8': @@ -4288,180 +3908,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@standard-schema/utils@0.3.0': {} - - '@tailwindcss/node@4.1.7': - dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.1 - jiti: 2.4.2 - lightningcss: 1.30.1 - magic-string: 0.30.17 - source-map-js: 1.2.1 - tailwindcss: 4.1.7 - - '@tailwindcss/oxide-android-arm64@4.1.7': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.1.7': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.1.7': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.1.7': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.1.7': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.1.7': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': - optional: true - - '@tailwindcss/oxide@4.1.7': - dependencies: - detect-libc: 2.0.4 - tar: 7.4.3 - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-x64': 4.1.7 - '@tailwindcss/oxide-freebsd-x64': 4.1.7 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-x64-musl': 4.1.7 - '@tailwindcss/oxide-wasm32-wasi': 4.1.7 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 - - '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1))': - dependencies: - '@tailwindcss/node': 4.1.7 - '@tailwindcss/oxide': 4.1.7 - tailwindcss: 4.1.7 - vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1) - - '@tanstack/eslint-plugin-router@1.115.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': - dependencies: - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - transitivePeerDependencies: - - supports-color - - typescript - - '@tanstack/history@1.115.0': {} - - '@tanstack/react-router-devtools@1.120.5(@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.120.5)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)': - dependencies: - '@tanstack/react-router': 1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/router-devtools-core': 1.120.5(@tanstack/router-core@1.120.5)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - solid-js: 1.9.7 - transitivePeerDependencies: - - '@tanstack/router-core' - - csstype - - tiny-invariant - - '@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@tanstack/history': 1.115.0 - '@tanstack/react-store': 0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/router-core': 1.120.5 - jsesc: 3.1.0 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - '@tanstack/react-store@0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@tanstack/store': 0.7.0 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) - - '@tanstack/router-core@1.120.5': - dependencies: - '@tanstack/history': 1.115.0 - '@tanstack/store': 0.7.0 - tiny-invariant: 1.3.3 - - '@tanstack/router-devtools-core@1.120.5(@tanstack/router-core@1.120.5)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': - dependencies: - '@tanstack/router-core': 1.120.5 - clsx: 2.1.1 - goober: 2.1.16(csstype@3.1.3) - solid-js: 1.9.7 - tiny-invariant: 1.3.3 - optionalDependencies: - csstype: 3.1.3 - - '@tanstack/router-generator@1.120.5(@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': - dependencies: - '@tanstack/virtual-file-routes': 1.115.0 - prettier: 3.5.3 - tsx: 4.19.4 - zod: 3.24.4 - optionalDependencies: - '@tanstack/react-router': 1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - - '@tanstack/router-plugin@1.120.5(@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1))': - dependencies: - '@babel/core': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.1) - '@babel/template': 7.27.2 - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 - '@tanstack/router-core': 1.120.5 - '@tanstack/router-generator': 1.120.5(@tanstack/react-router@1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) - '@tanstack/router-utils': 1.115.0 - '@tanstack/virtual-file-routes': 1.115.0 - '@types/babel__core': 7.20.5 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 - babel-dead-code-elimination: 1.0.10 - chokidar: 3.6.0 - unplugin: 2.3.4 - zod: 3.24.4 - optionalDependencies: - '@tanstack/react-router': 1.120.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - '@tanstack/router-utils@1.115.0': - dependencies: - '@babel/generator': 7.27.1 - '@babel/parser': 7.27.2 - ansis: 3.17.0 - diff: 7.0.0 - - '@tanstack/store@0.7.0': {} - - '@tanstack/virtual-file-routes@1.115.0': {} - '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -4802,8 +4248,6 @@ snapshots: ansi-styles@6.2.1: {} - ansis@3.17.0: {} - anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -4910,15 +4354,6 @@ snapshots: '@fastify/error': 4.1.0 fastq: 1.19.1 - babel-dead-code-elimination@1.0.10: - dependencies: - '@babel/core': 7.27.1 - '@babel/parser': 7.27.2 - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 - transitivePeerDependencies: - - supports-color - balanced-match@1.0.2: {} bcryptjs@3.0.2: {} @@ -4999,8 +4434,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chownr@3.0.0: {} - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -5010,8 +4443,6 @@ snapshots: slice-ansi: 7.1.2 string-width: 8.1.0 - clsx@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5044,8 +4475,6 @@ snapshots: csstype@3.1.3: {} - daisyui@5.0.35: {} - data-uri-to-buffer@4.0.1: {} data-view-buffer@1.0.2: @@ -5098,12 +4527,11 @@ snapshots: detect-libc@2.0.2: {} - detect-libc@2.0.4: {} + detect-libc@2.0.4: + optional: true diff@4.0.2: {} - diff@7.0.0: {} - doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -5152,11 +4580,6 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.18.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - entities@4.5.0: {} environment@1.1.0: {} @@ -5741,10 +5164,6 @@ snapshots: globrex@0.1.2: {} - goober@2.1.16(csstype@3.1.3): - dependencies: - csstype: 3.1.3 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -5962,7 +5381,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@2.4.2: {} + jiti@2.4.2: + optional: true jju@1.4.0: {} @@ -6085,6 +5505,7 @@ snapshots: lightningcss-linux-x64-musl: 1.30.1 lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + optional: true lint-staged@16.2.6: dependencies: @@ -6143,10 +5564,6 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.507.0(react@19.1.0): - dependencies: - react: 19.1.0 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -6192,14 +5609,8 @@ snapshots: minipass@7.1.2: {} - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - mkdirp@1.0.4: {} - mkdirp@3.0.1: {} - mlly@1.7.4: dependencies: acorn: 8.14.1 @@ -6211,16 +5622,6 @@ snapshots: dependencies: obliterator: 2.0.5 - mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - mobx: 6.13.7 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) - optionalDependencies: - react-dom: 19.1.0(react@19.1.0) - - mobx@6.13.7: {} - mrmime@2.0.1: {} ms@2.1.3: {} @@ -6430,10 +5831,6 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 - react-hook-form@7.56.4(react@19.1.0): - dependencies: - react: 19.1.0 - react-is@16.13.1: {} react-refresh@0.17.0: {} @@ -6570,12 +5967,6 @@ snapshots: semver@7.7.2: {} - seroval-plugins@1.3.1(seroval@1.3.1): - dependencies: - seroval: 1.3.1 - - seroval@1.3.1: {} - set-cookie-parser@2.7.1: {} set-function-length@1.2.2: @@ -6649,12 +6040,6 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.1.0 - solid-js@1.9.7: - dependencies: - csstype: 3.1.3 - seroval: 1.3.1 - seroval-plugins: 1.3.1(seroval@1.3.1) - sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -6785,21 +6170,6 @@ snapshots: dependencies: '@pkgr/core': 0.2.4 - tailwind-merge@3.3.0: {} - - tailwindcss@4.1.7: {} - - tapable@2.2.1: {} - - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 - test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -6810,10 +6180,6 @@ snapshots: dependencies: real-require: 0.2.0 - tiny-invariant@1.3.3: {} - - tiny-warning@1.0.3: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -6974,12 +6340,6 @@ snapshots: universalify@2.0.1: {} - unplugin@2.3.4: - dependencies: - acorn: 8.14.1 - picomatch: 4.0.2 - webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.3(browserslist@4.24.5): dependencies: browserslist: 4.24.5 @@ -6990,10 +6350,6 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.5.0(react@19.1.0): - dependencies: - react: 19.1.0 - v8-compile-cache-lib@3.0.1: {} vite-node@3.2.4(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.1): @@ -7109,8 +6465,6 @@ snapshots: web-streams-polyfill@3.3.3: {} - webpack-virtual-modules@0.6.2: {} - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -7191,8 +6545,6 @@ snapshots: yallist@4.0.0: {} - yallist@5.0.0: {} - yaml@2.8.1: {} yn@3.1.1: {} From b9278a414f7b7472ada4653626faebe510f3b607 Mon Sep 17 00:00:00 2001 From: gorushkin Date: Mon, 27 Jul 2026 11:59:45 +0300 Subject: [PATCH 30/30] LED-116 chore: clean up code changes sections --- .../infrastructure/db/commodities/commodity.repository.ts | 2 +- .../presentation/http/accounts/account.controller.test.ts | 6 +++--- .../src/presentation/http/accounts/account.controller.ts | 2 +- .../src/presentation/http/accounts/account.routes.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts index e58fb5ea..5587fe5d 100644 --- a/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts +++ b/apps/backend/src/infrastructure/db/commodities/commodity.repository.ts @@ -165,6 +165,6 @@ export class CommodityRepository ); return CommodityPersistenceMapper.toSnapshot(existingCommodity); - }, 'Failed to soft delete commodity'); + }, `Failed to soft delete commodity with ID ${commodityId}`); } } diff --git a/apps/backend/src/presentation/http/accounts/account.controller.test.ts b/apps/backend/src/presentation/http/accounts/account.controller.test.ts index 5f5f77bd..3bdcd86a 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.test.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.test.ts @@ -179,11 +179,11 @@ describe('AccountController', () => { }); }); - describe('deleteAccount', () => { + describe('archiveAccount', () => { it('should call archive account use case with correct account id and user', async () => { mockArchiveAccountUseCase.execute.mockResolvedValue(undefined); - await accountController.deleteAccount(user, { id: accountId }); + await accountController.archiveAccount(user, { id: accountId }); expect(mockArchiveAccountUseCase.execute).toHaveBeenCalledWith( user, @@ -194,7 +194,7 @@ describe('AccountController', () => { it('should throw ZodError for invalid request params', async () => { await expect( - accountController.deleteAccount(user, { id: 'not-a-uuid' }), + accountController.archiveAccount(user, { id: 'not-a-uuid' }), ).rejects.toThrow(ZodError); expect(mockArchiveAccountUseCase.execute).not.toHaveBeenCalled(); diff --git a/apps/backend/src/presentation/http/accounts/account.controller.ts b/apps/backend/src/presentation/http/accounts/account.controller.ts index 7a4aba6e..4387d60e 100644 --- a/apps/backend/src/presentation/http/accounts/account.controller.ts +++ b/apps/backend/src/presentation/http/accounts/account.controller.ts @@ -44,7 +44,7 @@ export class AccountController { return this.updateAccountUseCase.execute(user, id, accountUpdateDto); } - async deleteAccount(user: User, requestParams: unknown) { + async archiveAccount(user: User, requestParams: unknown) { const { id } = uniqueIdSchema.parse(requestParams); return this.archiveAccountUseCase.execute(user, id); diff --git a/apps/backend/src/presentation/http/accounts/account.routes.ts b/apps/backend/src/presentation/http/accounts/account.routes.ts index 24c59397..f7570eed 100644 --- a/apps/backend/src/presentation/http/accounts/account.routes.ts +++ b/apps/backend/src/presentation/http/accounts/account.routes.ts @@ -25,7 +25,7 @@ export const accountsRoutes = (app: FastifyInstance) => { app.delete('/:id', async (request, reply) => { const user = request.user; - await accountController.deleteAccount(user, request.params); + await accountController.archiveAccount(user, request.params); reply.status(204).send(); });