@spinekit/assets 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 — 2026-08-20
4
+
5
+ ### Changed
6
+ - **License:** relicensed from MIT to the **Classytic Source-Available License**
7
+ (Community & Commercial). Evaluation and development use remain free; production
8
+ use now requires a commercial license from Classytic LLC. See `LICENSE`.
9
+
10
+ ### Notes
11
+ - First stable **1.0.0** release of the Classytic `arc` suite. Versions published
12
+ before 1.0.0 remain under their original MIT terms; the new license applies from
13
+ 1.0.0 onward.
14
+
15
+ ## 0.2.0 — 2026-07-20 (amended 2026-07-21, pre-publish)
16
+
17
+ - **Engine teardown symmetry**: a module-created engine now registers `onClose` → `engine.destroy()` (closes the kernel's event transport); a BYO engine is never closed by the module.
18
+
19
+ - **scheduledJobs arm**: assetsMaintenanceSchedules (automated depreciation posting, daily) wired into createAssetsModule (factory form — resolves post-bootstrap); opt out with maintenance: false (the deployment-tier decision stays host policy). Peer floor raised @classytic/arc >=2.24.0.
20
+ - TENANCY: sweeps default to a bare cron context; tenant-enabled engines (the kernel default) MUST pass maintenance.context (org scoping) or set maintenance:false — otherwise every tick fails behind arc's fail-open scheduler. Company-wide hosts need nothing.
21
+ - **owns colocation (PACKAGE_RULES §1.5)**: createAssetsModule now declares `ArcModule.owns: ['fixed-asset']`, mirroring its single resource. Hosts remove their hand-maintained supersession lists.
22
+ - Long-interval schedules set explicit leaseMs (~90% of every) — arc's default lease caps at 5min, which would let independently-phased replicas re-run hourly/daily sweeps.
23
+ - Sweep is sequential per asset (idempotent via dep:<scheduleId>:<sequence>); large registers (10k+ assets) should expect a long daily tick — batching is a known future hardening.
24
+
25
+ ## 0.1.1 — 2026-07-18
26
+
27
+ - Peer floor bumps: `@classytic/arc >=2.23.0`, `@classytic/assets >=0.4.1`,
28
+ `@classytic/mongokit >=3.23.0` (tracks assets 0.4 API surface + arc 2.23
29
+ module system).
30
+ - Integrated `permissionMatrix` from `@classytic/arc/permissions` for
31
+ fine-grained action-level gates on lifecycle transition and valuation routes.
32
+ - Dev toolchain: `tsdown ^0.22.5`, `typescript ^7.0.2`.
33
+
34
+ ## 0.1.1 — 2026-07-11
35
+
36
+ Initial release.
37
+
38
+ - **`createAssetsModule(deps)`** — single composition surface that boots the
39
+ `@classytic/assets` engine (or accepts a BYO engine via `deps.engine` for
40
+ strangler migrations) and mounts an arc resource serving the full fixed-asset
41
+ register API: acquisition, lifecycle transitions (commission, place-in-service,
42
+ retire, dispose), 6-method multi-book depreciation (straight-line, declining-
43
+ balance, double-declining, sum-of-years, units-of-production, MACRS), IAS 16
44
+ revaluation, IAS 36 impairment, IAS 8 estimate changes, market/appraisal value
45
+ history, and disposal with gain/loss calculation.
46
+ - **Ledger bridge injection** — `deps.ledger` (sync or `(app) => LedgerPort`) wires
47
+ journal posting for depreciation + disposal entries; omit to run without GL
48
+ posting (read-only register or deferred wiring).
49
+ - **Host-policy surface** — `currency`, `categoryEnum`, `tenantField`, `prefix`,
50
+ `schemaOptions` let the host inject IAS classification codes, functional currency,
51
+ and tenant scoping without forking the module.
52
+ - **Event transport + outbox** — `deps.eventTransport` / `deps.outbox` thread through
53
+ to the engine for durable lifecycle event delivery.
54
+ - Peers: `@classytic/arc >=2.20.0`, `@classytic/assets >=0.3.0`,
55
+ `@classytic/mongokit >=3.19.0`, `mongoose >=9.4.1`.
package/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Classytic Source-Available License (Community & Commercial), Version 1.0
2
+
3
+ Copyright (c) 2026 Classytic LLC. All rights reserved.
4
+
5
+ This software is source-available, not open source. Please read this License
6
+ before using it.
7
+
8
+ 1. DEFINITIONS
9
+ "Software" means the contents of this package and any accompanying source
10
+ code, object code, or documentation, in any form.
11
+ "Licensor" means Classytic LLC, a limited liability company organized in the
12
+ United States.
13
+ "You" means the individual or legal entity exercising rights under this
14
+ License.
15
+ "Evaluation Use" means use for evaluation, development, testing,
16
+ experimentation, or internal non-commercial purposes only.
17
+ "Production Use" means any use of the Software that is live, commercial,
18
+ revenue-generating, or customer-facing, or any use other than Evaluation Use.
19
+ "Commercial License" means a separate written agreement executed with
20
+ Licensor that grants Production Use rights.
21
+
22
+ 2. OWNERSHIP
23
+ The Software is licensed, not sold. Licensor retains all right, title, and
24
+ interest in and to the Software, including all intellectual property rights.
25
+ All rights not expressly granted in this License are reserved by Licensor.
26
+
27
+ 3. EVALUATION GRANT
28
+ Subject to Your compliance with this License, Licensor grants You a
29
+ worldwide, royalty-free, non-exclusive, non-transferable, revocable license
30
+ to download, install, run, and modify the Software solely for Evaluation Use.
31
+
32
+ 4. PRODUCTION USE REQUIRES A COMMERCIAL LICENSE
33
+ Production Use of the Software is NOT permitted under this License and
34
+ requires a Commercial License from Licensor. A Commercial License may be
35
+ granted at Licensor's discretion, with or without a fee, and may confer
36
+ perpetual, irrevocable, and transferable Production Use rights as set out in
37
+ that agreement. Contact licensing@classytic.com to obtain one.
38
+
39
+ 5. RESTRICTIONS
40
+ You may not:
41
+ (a) make any Production Use of the Software without a Commercial License;
42
+ (b) redistribute, resell, rent, sublicense, or otherwise make the Software
43
+ (in whole or in part, original or modified) available to third parties as
44
+ a stand-alone product, framework, library, or hosted/managed service that
45
+ competes with the Software or with Licensor;
46
+ (c) remove, obscure, or alter any copyright, license, or attribution notice.
47
+
48
+ 6. FEEDBACK AND CONTRIBUTIONS
49
+ Any feedback, suggestions, or contributions You provide relating to the
50
+ Software may be used by Licensor for any purpose without obligation to You.
51
+
52
+ 7. TERMINATION
53
+ This License and the Evaluation Grant terminate automatically if You breach
54
+ any term. Upon termination You must cease all use of the Software and destroy
55
+ all copies in Your possession. Rights under a Commercial License are governed
56
+ by that agreement's own terms and survive termination of this License to the
57
+ extent stated there.
58
+
59
+ 8. DISCLAIMER OF WARRANTY
60
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
63
+
64
+ 9. LIMITATION OF LIABILITY
65
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LICENSOR BE LIABLE
66
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
68
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69
+
70
+ 10. GOVERNING LAW
71
+ This License is governed by the laws of the State of Delaware, United States,
72
+ without regard to its conflict-of-laws principles. (Adjust to Your LLC's state
73
+ of formation.)
74
+
75
+ For commercial licensing, contact: licensing@classytic.com
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @spinekit/assets
2
+
3
+ > Arc module for @classytic/assets — fixed-asset register + multi-book depreciation + IAS 16/36 valuation as composable resources.
4
+
5
+ Part of **Spine**, Classytic's ERP — the arc-module layer composed into an
6
+ [`@classytic/arc`](https://www.npmjs.com/package/@classytic/arc) application via
7
+ `createApp({ modules })`.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @spinekit/assets @classytic/arc @classytic/mongokit mongoose zod
13
+ ```
14
+
15
+ Kernels and the framework are **peer dependencies** — the host installs a single
16
+ copy; this module bundles nothing.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { createApp } from '@classytic/arc/factory';
22
+ import { createAssetsModule } from '@spinekit/assets';
23
+
24
+ const app = await createApp({
25
+ modules: [
26
+ createAssetsModule({
27
+ connection, // your mongoose connection
28
+ permissions, // role gates
29
+ // BYO engine + host seams (extraActions, extraRoutes, bridges) as needed
30
+ }),
31
+ ],
32
+ });
33
+ ```
34
+
35
+ > This is a standard scaffold — expand Usage with the module's specific options.
36
+
37
+ ## License
38
+
39
+ Proprietary — Classytic Source-Available License (see [LICENSE](LICENSE)).
40
+ Free for evaluation/development; production use requires a commercial license
41
+ from Classytic LLC.
@@ -0,0 +1,110 @@
1
+ import { SpineTenantInput } from "@spinekit/kit/tenant";
2
+ import { AssetsContext, AssetsEngine, LedgerPort } from "@classytic/assets";
3
+ import { ScheduleDefinition } from "@classytic/arc/plugins";
4
+ import { PermissionGate } from "@spinekit/kit/permissions";
5
+ import { EventTransport } from "@classytic/primitives/events";
6
+ import { OutboxStore } from "@classytic/primitives/outbox";
7
+ import { Connection } from "mongoose";
8
+ import { FastifyInstance } from "fastify";
9
+ //#region src/schedules.d.ts
10
+ interface AssetsMaintenanceOptions {
11
+ /** Automated depreciation posting sweep interval. Default daily (with jitter). */
12
+ depreciationEvery?: number;
13
+ /**
14
+ * Extra kernel-context fields merged over the default `{ actorId: 'cron' }`.
15
+ * REQUIRED when the host composes the register tenant-scoped (`tenantField`
16
+ * set): an org-less sweep then fails per-asset under the tenant plugin —
17
+ * `posted: 0` forever with only error logs. Company-wide registers
18
+ * (`tenantField: false`, the module default) need nothing.
19
+ */
20
+ context?: Partial<AssetsContext>;
21
+ }
22
+ declare function assetsMaintenanceSchedules(engine: AssetsEngine, options?: AssetsMaintenanceOptions): ScheduleDefinition[];
23
+ //#endregion
24
+ //#region src/index.d.ts
25
+ interface AssetsPermissions {
26
+ /** Read register / schedules / valuations. */
27
+ view: PermissionGate;
28
+ /** Acquire, lifecycle transitions, valuation, onboard. */
29
+ manage: PermissionGate;
30
+ }
31
+ interface AssetsModuleDeps {
32
+ connection: Connection;
33
+ /** BYO engine (strangler) - module skips engine creation. */
34
+ engine?: AssetsEngine;
35
+ permissions: AssetsPermissions;
36
+ /**
37
+ * GL bridge (host policy - category -> chart account). Function form
38
+ * receives the live app so a host composing arc-accounting FIRST wires
39
+ * off f.arc.modules.accounting. Omit to run without journal posting.
40
+ */
41
+ ledger?: LedgerPort | ((app: FastifyInstance) => LedgerPort | undefined);
42
+ /**
43
+ * Modules that MUST bootstrap before this one (arc 2.20 topological order).
44
+ *
45
+ * Defaults to `['accounting']` when `ledger` is given in FACTORY form, since
46
+ * that factory reads `f.arc.modules.accounting`. Without the edge the
47
+ * factory can receive an undefined module and yield `undefined`, which
48
+ * silently disables depreciation/disposal journal posting on a financial
49
+ * register. Pass `[]` to opt out, or a list to add edges.
50
+ */
51
+ dependsOn?: readonly string[];
52
+ /** Functional currency for money fields (ISO 4217). Host-owned — a money
53
+ * module must know its currency; there is no jurisdiction default. */
54
+ currency: string;
55
+ /** Allowed category values (host policy - NBR/IFRS classes). */
56
+ categoryEnum?: readonly string[];
57
+ /** Canonical persistence tenancy. Default false (company-wide register). */
58
+ tenant?: SpineTenantInput;
59
+ /** Route prefix. Default /assets. */
60
+ prefix?: string;
61
+ /** Extra mongoose schema fields (e.g. host category enum on category). */
62
+ schemaOptions?: Record<string, unknown>;
63
+ eventTransport?: EventTransport;
64
+ outbox?: OutboxStore;
65
+ /**
66
+ * Re-register the asset models when they are already on the connection.
67
+ *
68
+ * Mongoose locks a schema on the first `model()` call, so a second
69
+ * registration cannot add fields or indexes — it can only silently drop them,
70
+ * which is why mongokit refuses instead. Right for production; wrong for a
71
+ * suite that boots the app twice on one connection, and this module had no way
72
+ * to say so. (mongokit itself refuses `replace` under NODE_ENV=production, so
73
+ * this cannot become a production hot-reload path.)
74
+ */
75
+ forceRecreate?: boolean;
76
+ /**
77
+ * Accept a backend without multi-document transactions. DEV/TEST ONLY.
78
+ *
79
+ * Depreciation posting writes the schedule row and its journal entry
80
+ * together, so the kernel wants a replica set. Absent, this module built an
81
+ * engine that REFUSED to bind on a standalone mongod — the host could only
82
+ * get past it by constructing the engine itself, which is exactly the BYO
83
+ * escape hatch this arm removes the need for.
84
+ */
85
+ allowNonTransactional?: boolean;
86
+ /**
87
+ * Automated-depreciation sweep interval. Omit for the daily default; pass
88
+ * `false` to opt OUT of the `scheduledJobs` arm — the DEPLOYMENT-TIER
89
+ * decision (auto-posting vs the manual `post-period` action) is host policy.
90
+ * See `assetsMaintenanceSchedules`.
91
+ */
92
+ maintenance?: AssetsMaintenanceOptions | false;
93
+ }
94
+ /**
95
+ * Build the fixed-asset resource over a live assets engine. Exported so a
96
+ * host can mount it directly (custom module composition).
97
+ */
98
+ declare function createAssetResource(args: {
99
+ engine: AssetsEngine;
100
+ permissions: AssetsPermissions;
101
+ /** ISO 4217 — host-owned, required (a money module has no default currency). */
102
+ currency: string;
103
+ categoryEnum?: readonly string[];
104
+ prefix?: string;
105
+ }): import("@classytic/arc").ResourceDefinition<import("@classytic/assets").AssetDocument | (import("mongoose").Document<unknown, {}, import("@classytic/assets").AssetDocument, {}, import("mongoose").DefaultSchemaOptions> & import("@classytic/assets").Asset & import("mongoose").Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & {
106
+ id: string;
107
+ })>;
108
+ declare function createAssetsModule(deps: AssetsModuleDeps): import("@classytic/arc/factory").ArcModule<AssetsEngine>;
109
+ //#endregion
110
+ export { type AssetsMaintenanceOptions, AssetsModuleDeps, AssetsPermissions, assetsMaintenanceSchedules, createAssetResource, createAssetsModule };
package/dist/index.mjs ADDED
@@ -0,0 +1,817 @@
1
+ import { defineEngineModule } from "@spinekit/kit/engine-module";
2
+ import { resolveSpineTenant } from "@spinekit/kit/tenant";
3
+ import { defineResource } from "@classytic/arc";
4
+ import { permissionMatrix } from "@classytic/arc/permissions";
5
+ import { scopeFirstCtx } from "@classytic/arc/scope";
6
+ import { ValidationError } from "@classytic/arc/utils";
7
+ import { createMongooseAdapter } from "@classytic/mongokit/adapter";
8
+ import { QueryParser } from "@classytic/mongokit";
9
+ import { DEPRECIATION_METHODS, defineAssets } from "@classytic/assets";
10
+ import { isCurrencyCode } from "@classytic/primitives/currency";
11
+ import { money } from "@classytic/primitives/money";
12
+ //#region src/schedules.ts
13
+ const DAY = 864e5;
14
+ function assetsMaintenanceSchedules(engine, options = {}) {
15
+ return [{
16
+ name: "assets.depreciation",
17
+ every: options.depreciationEvery ?? DAY,
18
+ leaseMs: Math.floor((options.depreciationEvery ?? DAY) * .9),
19
+ jitterMs: 9e5,
20
+ handler: async (fastify) => {
21
+ const ctx = {
22
+ actorId: "cron",
23
+ ...options.context
24
+ };
25
+ const now = /* @__PURE__ */ new Date();
26
+ const active = await engine.models.FixedAsset.find({
27
+ status: "active",
28
+ deletedAt: null
29
+ }).select("_id name").lean();
30
+ let posted = 0;
31
+ for (const asset of active) try {
32
+ const schedule = await engine.repositories.schedule.findByAssetAndBook(String(asset._id), "accounting", ctx);
33
+ if (!schedule) continue;
34
+ const due = schedule.periods.filter((per) => per.status === "scheduled" && new Date(per.periodEnd) <= now).sort((a, b) => a.sequence - b.sequence);
35
+ for (const per of due) {
36
+ await engine.services.depreciation.postPeriod(String(asset._id), per.sequence, {}, ctx);
37
+ posted += 1;
38
+ }
39
+ } catch (err) {
40
+ fastify.log.error({
41
+ err,
42
+ assetId: String(asset._id)
43
+ }, "Depreciation posting failed for asset");
44
+ }
45
+ if (posted > 0) fastify.log.info({ posted }, "Depreciation periods auto-posted");
46
+ }
47
+ }];
48
+ }
49
+ //#endregion
50
+ //#region src/index.ts
51
+ const createAssetWireBody = {
52
+ type: "object",
53
+ required: [
54
+ "assetNumber",
55
+ "name",
56
+ "depreciation"
57
+ ],
58
+ properties: {
59
+ assetNumber: {
60
+ type: "string",
61
+ minLength: 1
62
+ },
63
+ name: {
64
+ type: "string",
65
+ minLength: 1
66
+ },
67
+ category: {
68
+ type: "string",
69
+ description: "Default: 'office_equipment'"
70
+ },
71
+ cost: {
72
+ type: "integer",
73
+ minimum: 0,
74
+ description: "Acquisition cost in minor units (paisa)"
75
+ },
76
+ salvageValue: {
77
+ type: "integer",
78
+ minimum: 0,
79
+ description: "Salvage value in minor units"
80
+ },
81
+ depreciation: {
82
+ type: "object",
83
+ required: ["method", "periods"],
84
+ properties: {
85
+ method: {
86
+ type: "string",
87
+ enum: [...DEPRECIATION_METHODS]
88
+ },
89
+ periods: {
90
+ type: "integer",
91
+ minimum: 1
92
+ },
93
+ decliningRate: {
94
+ type: "number",
95
+ exclusiveMinimum: 0,
96
+ exclusiveMaximum: 1
97
+ },
98
+ totalUnits: {
99
+ type: "integer",
100
+ minimum: 1
101
+ },
102
+ manualSchedule: {
103
+ type: "array",
104
+ items: { type: "integer" }
105
+ }
106
+ }
107
+ },
108
+ placedInServiceAt: {
109
+ type: "string",
110
+ description: "Date — parsed by the service"
111
+ },
112
+ location: { type: "string" },
113
+ custodian: { type: "string" },
114
+ branchId: { type: "string" },
115
+ sourceId: { type: "string" },
116
+ sourceModel: { type: "string" },
117
+ metadata: {
118
+ type: "object",
119
+ additionalProperties: true
120
+ }
121
+ }
122
+ };
123
+ const updateAssetWireBody = {
124
+ type: "object",
125
+ properties: {
126
+ name: {
127
+ type: "string",
128
+ minLength: 1
129
+ },
130
+ category: { type: "string" },
131
+ location: { type: "string" },
132
+ custodian: { type: "string" },
133
+ metadata: {
134
+ type: "object",
135
+ additionalProperties: true
136
+ }
137
+ }
138
+ };
139
+ /**
140
+ * Build the fixed-asset resource over a live assets engine. Exported so a
141
+ * host can mount it directly (custom module composition).
142
+ */
143
+ function createAssetResource(args) {
144
+ const { engine, permissions } = args;
145
+ const currency = args.currency;
146
+ if (!isCurrencyCode(currency)) throw new Error(`createAssetResource: \`currency\` must be an ISO 4217 code, got ${JSON.stringify(currency)}. It is host config with no default -- an asset register must know its functional currency.`);
147
+ const categoryEnum = args.categoryEnum;
148
+ const prefix = args.prefix;
149
+ const money$1 = (amount) => money(amount, currency);
150
+ /** Repo verbs return hydrated docs OR lean claim() results - normalize. */
151
+ const plain = (d) => {
152
+ const doc = d;
153
+ return typeof doc?.toObject === "function" ? doc.toObject() : d;
154
+ };
155
+ /**
156
+ * Actor context from the arc request (company-wide - no org scope).
157
+ *
158
+ * Scope-FIRST (arc 2.25): `request.scope` is the validated identity every
159
+ * auth adapter populates, so this resolves a human `userId` OR a service
160
+ * `clientId`. Reading `request.user._id` directly — as this did before —
161
+ * silently attributed EVERY fixed-asset mutation to `'system'` on any host
162
+ * using scope-based auth (service tokens, API keys), destroying the audit
163
+ * trail on a financial register. `scopeFirstCtx` is structural: it accepts a
164
+ * raw `FastifyRequest` AND arc's flat `IRequestContext` projection, so one
165
+ * resolver serves routes, actions, and controller overrides alike.
166
+ */
167
+ const ctxOf = (req) => scopeFirstCtx(req, { fallbackActorId: "system" });
168
+ const DEPRECIATION_SCHEMA = {
169
+ type: "object",
170
+ required: ["method", "periods"],
171
+ properties: {
172
+ method: {
173
+ type: "string",
174
+ enum: [
175
+ "straight_line",
176
+ "declining_balance",
177
+ "double_declining",
178
+ "sum_of_years_digits",
179
+ "units_of_production",
180
+ "manual"
181
+ ]
182
+ },
183
+ periods: {
184
+ type: "integer",
185
+ minimum: 1,
186
+ maximum: 1200
187
+ },
188
+ decliningRate: {
189
+ type: "number",
190
+ exclusiveMinimum: 0,
191
+ maximum: 1
192
+ },
193
+ totalUnits: {
194
+ type: "integer",
195
+ minimum: 1
196
+ },
197
+ manualSchedule: {
198
+ type: "array",
199
+ items: {
200
+ type: "integer",
201
+ minimum: 0
202
+ }
203
+ }
204
+ },
205
+ additionalProperties: false
206
+ };
207
+ /**
208
+ * CREATE is the domain verb, declared as a WRITE VERB (arc 2.34 `writes`)
209
+ * rather than a controller override.
210
+ *
211
+ * The override replaced arc's whole write pipeline for this slot, so the
212
+ * seven `systemManaged: true` rules below were declared and never enforced on
213
+ * `POST /` — as were tenant injection, `createdBy` and the hook sandwich.
214
+ * `acquire()` happens to build an explicit whitelist, so nothing forged got
215
+ * through in practice; that is the kernel saving the resource, not the
216
+ * resource being safe.
217
+ *
218
+ * It also fixed a drift the override could not: the controller hardcoded
219
+ * `tenantField: false` while the resource declares `tenantField ?? false`, so
220
+ * a host that passed one got a resource scoped by it and a controller that
221
+ * ignored it. Arc auto-builds the controller now, from the resource's own
222
+ * value.
223
+ */
224
+ const createFixedAsset = async (data, ctx) => {
225
+ const asset = await engine.repositories.asset.acquire({
226
+ assetNumber: String(data.assetNumber ?? ""),
227
+ name: String(data.name ?? ""),
228
+ category: data.category ?? "office_equipment",
229
+ cost: money$1(Number(data.cost ?? 0)),
230
+ salvageValue: money$1(Number(data.salvageValue ?? 0)),
231
+ depreciation: data.depreciation,
232
+ ...data.placedInServiceAt ? { placedInServiceAt: new Date(String(data.placedInServiceAt)) } : {},
233
+ ...data.branchId ? { branchId: String(data.branchId) } : {},
234
+ ...data.location ? { location: String(data.location) } : {},
235
+ ...data.custodian ? { custodian: String(data.custodian) } : {},
236
+ ...data.metadata ? { metadata: data.metadata } : {}
237
+ }, ctxOf(ctx));
238
+ return {
239
+ ...plain(asset),
240
+ id: String(asset._id)
241
+ };
242
+ };
243
+ return defineResource({
244
+ name: "fixed-asset",
245
+ displayName: "Fixed Assets",
246
+ tag: "Finance - Fixed Assets",
247
+ prefix: prefix ?? "/assets",
248
+ audit: true,
249
+ adapter: createMongooseAdapter({
250
+ model: engine.models.FixedAsset,
251
+ repository: engine.repositories.asset
252
+ }),
253
+ customSchemas: {
254
+ create: { body: createAssetWireBody },
255
+ update: { body: updateAssetWireBody }
256
+ },
257
+ queryParser: new QueryParser({
258
+ maxLimit: 100,
259
+ allowedFilterFields: [
260
+ "status",
261
+ "category",
262
+ "assetNumber",
263
+ "branchId",
264
+ "location",
265
+ "custodian"
266
+ ]
267
+ }),
268
+ tenantField: resolveSpineTenant(engine.config.tenant).resourceTenantField,
269
+ /**
270
+ * The CREATE slot IS the kernel's `acquire()` verb — arc keeps the pipeline
271
+ * (field-rule sanitization, tenant injection, actor stamp, hooks) and calls
272
+ * the command where it would have called `repository.create`.
273
+ */
274
+ writes: { create: (data, wctx) => createFixedAsset(data, wctx.req) },
275
+ schemaOptions: { fieldRules: {
276
+ status: { systemManaged: true },
277
+ accumulatedDepreciation: { systemManaged: true },
278
+ bookValue: { systemManaged: true },
279
+ grossCarryingAmount: { systemManaged: true },
280
+ accumulatedImpairment: { systemManaged: true },
281
+ revaluationSurplus: { systemManaged: true },
282
+ version: { systemManaged: true }
283
+ } },
284
+ permissions: permissionMatrix({
285
+ read: permissions.view,
286
+ write: permissions.manage
287
+ }),
288
+ actions: {
289
+ /** draft → active. Optional body: { placedInServiceAt } (past dates OK). */
290
+ activate: {
291
+ permissions: permissions.manage,
292
+ handler: async (id, data, req) => {
293
+ const body = data ?? {};
294
+ return engine.repositories.asset.activate(id, ctxOf(req), body.placedInServiceAt ? new Date(body.placedInServiceAt) : void 0);
295
+ }
296
+ },
297
+ /** Compute + persist the depreciation projection (safe pre-activation). */
298
+ "generate-schedule": {
299
+ permissions: permissions.manage,
300
+ schema: {
301
+ type: "object",
302
+ properties: {
303
+ book: {
304
+ type: "string",
305
+ description: "Depreciation book — default 'accounting'; 'tax' is projection-only"
306
+ },
307
+ firstPeriodFraction: {
308
+ type: "number",
309
+ exclusiveMinimum: 0,
310
+ maximum: 1,
311
+ description: "Half-year rule = 0.5; mid-month proration"
312
+ },
313
+ methodOverride: { type: "string" },
314
+ periodsOverride: {
315
+ type: "integer",
316
+ minimum: 1
317
+ },
318
+ decliningRateOverride: {
319
+ type: "number",
320
+ exclusiveMinimum: 0,
321
+ maximum: 1
322
+ }
323
+ },
324
+ additionalProperties: false
325
+ },
326
+ handler: async (id, data, req) => engine.services.depreciation.generate(id, data ?? {}, ctxOf(req))
327
+ },
328
+ /**
329
+ * Post ONE due period → JE `Dr 66xx Depreciation / Cr 1359 Accumulated`
330
+ * (accounting book; tax-book posts are projection flips only).
331
+ * Sequential-only; crash-retry dedupes via `dep:<scheduleId>:<sequence>`.
332
+ */
333
+ "post-period": {
334
+ permissions: permissions.manage,
335
+ schema: {
336
+ type: "object",
337
+ required: ["sequence"],
338
+ properties: {
339
+ sequence: {
340
+ type: "integer",
341
+ minimum: 0
342
+ },
343
+ units: {
344
+ type: "integer",
345
+ minimum: 0
346
+ },
347
+ book: { type: "string" }
348
+ },
349
+ additionalProperties: false
350
+ },
351
+ handler: async (id, data, req) => {
352
+ const body = data;
353
+ return engine.services.depreciation.postPeriod(id, body.sequence, {
354
+ ...body.units != null ? { units: body.units } : {},
355
+ ...body.book ? { book: body.book } : {}
356
+ }, ctxOf(req));
357
+ }
358
+ },
359
+ /**
360
+ * active → disposed. The engine computes gain/loss and posts the
361
+ * BC-style derecognition JE via the ledger bridge (draft for finance
362
+ * review): Dr 1359 accumulated + Dr 1113 proceeds / Cr 13xx cost,
363
+ * plug → 6810 loss or 4304 gain. Idempotent on `disposal:<assetId>`.
364
+ */
365
+ dispose: {
366
+ permissions: permissions.manage,
367
+ schema: {
368
+ type: "object",
369
+ properties: {
370
+ proceeds: {
371
+ type: "integer",
372
+ minimum: 0,
373
+ description: "Sale proceeds in paisa (0 = scrapped)"
374
+ },
375
+ disposedAt: { type: "string" },
376
+ note: {
377
+ type: "string",
378
+ maxLength: 512
379
+ }
380
+ },
381
+ additionalProperties: false
382
+ },
383
+ handler: async (id, data, req) => {
384
+ const body = data ?? {};
385
+ const { asset, gainLoss, ledgerEntryRef } = await engine.repositories.asset.dispose(id, {
386
+ proceeds: money$1(body.proceeds ?? 0),
387
+ ...body.disposedAt ? { disposedAt: new Date(body.disposedAt) } : {},
388
+ ...body.note ? { note: body.note } : {}
389
+ }, ctxOf(req));
390
+ return {
391
+ ...plain(asset),
392
+ id: String(asset._id),
393
+ gainLoss,
394
+ disposalEntryRef: ledgerEntryRef
395
+ };
396
+ }
397
+ },
398
+ /** active → retired (kept on register, no longer depreciating). */
399
+ retire: {
400
+ permissions: permissions.manage,
401
+ handler: async (id, _data, req) => engine.repositories.asset.retire(id, ctxOf(req))
402
+ },
403
+ /** active → written_off (book value zeroed by decision; JE is finance's manual call). */
404
+ "write-off": {
405
+ permissions: permissions.manage,
406
+ handler: async (id, _data, req) => engine.repositories.asset.writeOff(id, ctxOf(req))
407
+ },
408
+ /** Move location / custodian / branch dimension. */
409
+ transfer: {
410
+ permissions: permissions.manage,
411
+ schema: {
412
+ type: "object",
413
+ properties: {
414
+ location: {
415
+ type: "string",
416
+ maxLength: 128
417
+ },
418
+ custodian: {
419
+ type: "string",
420
+ maxLength: 128
421
+ },
422
+ branchId: { type: "string" },
423
+ note: {
424
+ type: "string",
425
+ maxLength: 512
426
+ }
427
+ },
428
+ additionalProperties: false
429
+ },
430
+ handler: async (id, data, req) => engine.repositories.asset.transfer(id, data ?? {}, ctxOf(req))
431
+ },
432
+ /** Append an informational valuation (market / insurance / tax_assessed / liquidation). */
433
+ "record-valuation": {
434
+ permissions: permissions.view,
435
+ schema: {
436
+ type: "object",
437
+ required: ["kind", "value"],
438
+ properties: {
439
+ kind: {
440
+ type: "string",
441
+ enum: [
442
+ "market",
443
+ "insurance",
444
+ "tax_assessed",
445
+ "liquidation"
446
+ ]
447
+ },
448
+ value: {
449
+ type: "integer",
450
+ minimum: 0,
451
+ description: "Value in paisa"
452
+ },
453
+ asOf: { type: "string" },
454
+ basis: {
455
+ type: "string",
456
+ enum: [
457
+ "market_approach",
458
+ "cost_approach",
459
+ "income_approach",
460
+ "index",
461
+ "manual"
462
+ ]
463
+ },
464
+ appraiser: {
465
+ type: "string",
466
+ maxLength: 256
467
+ },
468
+ notes: {
469
+ type: "string",
470
+ maxLength: 1024
471
+ }
472
+ },
473
+ additionalProperties: false
474
+ },
475
+ handler: async (id, data, req) => {
476
+ const body = data;
477
+ return engine.repositories.valuation.record(id, {
478
+ kind: body.kind,
479
+ value: money$1(body.value),
480
+ ...body.asOf ? { asOf: new Date(body.asOf) } : {},
481
+ ...body.basis ? { basis: body.basis } : {},
482
+ ...body.appraiser ? { appraiser: body.appraiser } : {},
483
+ ...body.notes ? { notes: body.notes } : {}
484
+ }, ctxOf(req));
485
+ }
486
+ },
487
+ /** IAS 16 revaluation — restate carrying amount to fair value (prospective rebase). */
488
+ revalue: {
489
+ permissions: permissions.manage,
490
+ schema: {
491
+ type: "object",
492
+ required: ["fairValue"],
493
+ properties: {
494
+ fairValue: {
495
+ type: "integer",
496
+ minimum: 0,
497
+ description: "Fair value in paisa"
498
+ },
499
+ asOf: { type: "string" },
500
+ appraiser: {
501
+ type: "string",
502
+ maxLength: 256
503
+ },
504
+ notes: {
505
+ type: "string",
506
+ maxLength: 1024
507
+ },
508
+ remainingPeriods: {
509
+ type: "integer",
510
+ minimum: 1
511
+ }
512
+ },
513
+ additionalProperties: false
514
+ },
515
+ handler: async (id, data, req) => {
516
+ const body = data;
517
+ return engine.services.valuation.revalue(id, {
518
+ fairValue: money$1(body.fairValue),
519
+ ...body.asOf ? { asOf: new Date(body.asOf) } : {},
520
+ ...body.appraiser ? { appraiser: body.appraiser } : {},
521
+ ...body.notes ? { notes: body.notes } : {},
522
+ ...body.remainingPeriods != null ? { remainingPeriods: body.remainingPeriods } : {}
523
+ }, ctxOf(req));
524
+ }
525
+ },
526
+ /** IAS 36 impairment — write down to the recoverable amount (prospective rebase). */
527
+ impair: {
528
+ permissions: permissions.manage,
529
+ schema: {
530
+ type: "object",
531
+ required: ["recoverableAmount"],
532
+ properties: {
533
+ recoverableAmount: {
534
+ type: "integer",
535
+ minimum: 0,
536
+ description: "Recoverable amount in paisa"
537
+ },
538
+ asOf: { type: "string" },
539
+ appraiser: {
540
+ type: "string",
541
+ maxLength: 256
542
+ },
543
+ notes: {
544
+ type: "string",
545
+ maxLength: 1024
546
+ },
547
+ remainingPeriods: {
548
+ type: "integer",
549
+ minimum: 1
550
+ }
551
+ },
552
+ additionalProperties: false
553
+ },
554
+ handler: async (id, data, req) => {
555
+ const body = data;
556
+ return engine.services.valuation.impair(id, {
557
+ recoverableAmount: money$1(body.recoverableAmount),
558
+ ...body.asOf ? { asOf: new Date(body.asOf) } : {},
559
+ ...body.appraiser ? { appraiser: body.appraiser } : {},
560
+ ...body.notes ? { notes: body.notes } : {},
561
+ ...body.remainingPeriods != null ? { remainingPeriods: body.remainingPeriods } : {}
562
+ }, ctxOf(req));
563
+ }
564
+ },
565
+ /** IAS 36 impairment reversal — capped at the accumulated loss. */
566
+ "reverse-impairment": {
567
+ permissions: permissions.manage,
568
+ schema: {
569
+ type: "object",
570
+ required: ["amount"],
571
+ properties: {
572
+ amount: {
573
+ type: "integer",
574
+ minimum: 1,
575
+ description: "Reversal amount in paisa"
576
+ },
577
+ asOf: { type: "string" },
578
+ notes: {
579
+ type: "string",
580
+ maxLength: 1024
581
+ }
582
+ },
583
+ additionalProperties: false
584
+ },
585
+ handler: async (id, data, req) => {
586
+ const body = data;
587
+ return engine.services.valuation.reverseImpairment(id, {
588
+ amount: money$1(body.amount),
589
+ ...body.asOf ? { asOf: new Date(body.asOf) } : {},
590
+ ...body.notes ? { notes: body.notes } : {}
591
+ }, ctxOf(req));
592
+ }
593
+ },
594
+ /** IAS 8 estimate change — remaining life and/or salvage, prospective. */
595
+ "change-estimate": {
596
+ permissions: permissions.manage,
597
+ schema: {
598
+ type: "object",
599
+ properties: {
600
+ remainingPeriods: {
601
+ type: "integer",
602
+ minimum: 1
603
+ },
604
+ salvageValue: {
605
+ type: "integer",
606
+ minimum: 0,
607
+ description: "New salvage in paisa"
608
+ }
609
+ },
610
+ additionalProperties: false
611
+ },
612
+ handler: async (id, data, req) => {
613
+ const body = data ?? {};
614
+ return engine.services.valuation.changeEstimate(id, {
615
+ ...body.remainingPeriods != null ? { remainingPeriods: body.remainingPeriods } : {},
616
+ ...body.salvageValue != null ? { salvageValue: money$1(body.salvageValue) } : {}
617
+ }, ctxOf(req));
618
+ }
619
+ }
620
+ },
621
+ routes: [
622
+ {
623
+ method: "GET",
624
+ path: "/:id/schedule",
625
+ summary: "Depreciation schedule (all periods with posting state; ?book=tax for alternate books)",
626
+ permissions: permissions.view,
627
+ rawHandler: async (req, reply) => {
628
+ const { id } = req.params;
629
+ const { book } = req.query;
630
+ const schedule = await engine.repositories.schedule.findByAssetAndBook(id, book ?? "accounting", {});
631
+ return reply.send(schedule ?? null);
632
+ }
633
+ },
634
+ {
635
+ method: "GET",
636
+ path: "/:id/valuations",
637
+ summary: "Append-only valuation history (market/insurance/tax + revaluation/impairment events)",
638
+ permissions: permissions.view,
639
+ rawHandler: async (req, reply) => {
640
+ const { id } = req.params;
641
+ const rows = await engine.models.FixedAssetValuation.find({ assetId: id }).sort({ asOf: -1 }).lean();
642
+ return reply.send(rows);
643
+ }
644
+ },
645
+ {
646
+ method: "GET",
647
+ path: "/:id/current-value",
648
+ summary: "Value resolution — ?kind=book|market|insurance|tax_assessed|liquidation (&asOf=ISO)",
649
+ permissions: permissions.view,
650
+ rawHandler: async (req, reply) => {
651
+ const { id } = req.params;
652
+ const { kind, asOf } = req.query;
653
+ const result = await engine.services.valuation.currentValue(id, {
654
+ ...kind ? { kind } : {},
655
+ ...asOf ? { asOf: new Date(asOf) } : {}
656
+ }, {});
657
+ return reply.send(result);
658
+ }
659
+ },
660
+ {
661
+ method: "POST",
662
+ path: "/onboard",
663
+ summary: "Onboard an EXISTING asset at go-live (net method — no JEs; GL values come from the opening-balance batch)",
664
+ permissions: permissions.manage,
665
+ schema: { body: {
666
+ type: "object",
667
+ required: [
668
+ "assetNumber",
669
+ "name",
670
+ "category",
671
+ "originalCost",
672
+ "accumulatedDepreciation",
673
+ "placedInServiceAt",
674
+ "depreciation"
675
+ ],
676
+ properties: {
677
+ assetNumber: {
678
+ type: "string",
679
+ minLength: 1,
680
+ maxLength: 64
681
+ },
682
+ name: {
683
+ type: "string",
684
+ minLength: 1,
685
+ maxLength: 256
686
+ },
687
+ category: categoryEnum ? {
688
+ type: "string",
689
+ enum: categoryEnum
690
+ } : { type: "string" },
691
+ originalCost: {
692
+ type: "integer",
693
+ minimum: 1,
694
+ description: "Historical acquisition cost (paisa)"
695
+ },
696
+ accumulatedDepreciation: {
697
+ type: "integer",
698
+ minimum: 0,
699
+ description: "Depreciation already taken pre-migration (paisa)"
700
+ },
701
+ salvageValue: {
702
+ type: "integer",
703
+ minimum: 0,
704
+ default: 0
705
+ },
706
+ placedInServiceAt: {
707
+ type: "string",
708
+ description: "Original in-service date (past)"
709
+ },
710
+ depreciation: DEPRECIATION_SCHEMA,
711
+ branchId: { type: "string" },
712
+ metadata: {
713
+ type: "object",
714
+ additionalProperties: true
715
+ }
716
+ },
717
+ additionalProperties: false
718
+ } },
719
+ rawHandler: async (req, reply) => {
720
+ const body = req.body;
721
+ const salvage = body.salvageValue ?? 0;
722
+ const remainingBookValue = body.originalCost - body.accumulatedDepreciation;
723
+ if (remainingBookValue < salvage) throw new ValidationError(`accumulatedDepreciation leaves book value (${remainingBookValue}) below salvage (${salvage})`);
724
+ const ctx = ctxOf(req);
725
+ const asset = await engine.repositories.asset.acquire({
726
+ assetNumber: body.assetNumber,
727
+ name: body.name,
728
+ category: body.category,
729
+ cost: money$1(remainingBookValue),
730
+ salvageValue: money$1(salvage),
731
+ depreciation: body.depreciation,
732
+ placedInServiceAt: new Date(body.placedInServiceAt),
733
+ ...body.branchId ? { branchId: body.branchId } : {},
734
+ metadata: {
735
+ ...body.metadata ?? {},
736
+ onboarded: true,
737
+ originalCost: body.originalCost,
738
+ accumulatedAtMigration: body.accumulatedDepreciation
739
+ }
740
+ }, ctx);
741
+ const id = String(asset._id);
742
+ const activated = await engine.repositories.asset.activate(id, ctx, new Date(body.placedInServiceAt));
743
+ const schedule = await engine.services.depreciation.generate(id, {}, ctx);
744
+ return reply.code(201).send({
745
+ asset: {
746
+ ...plain(activated),
747
+ id: String(activated._id)
748
+ },
749
+ schedule
750
+ });
751
+ }
752
+ }
753
+ ]
754
+ });
755
+ }
756
+ function createAssetsModule(deps) {
757
+ const maintenance = deps.maintenance;
758
+ const dependsOn = deps.dependsOn ?? (typeof deps.ledger === "function" ? ["accounting"] : []);
759
+ /**
760
+ * The live Fastify instance, captured for the FACTORY ledger form only.
761
+ *
762
+ * `defineEngineModule`'s `boot` is nullary by design, but a FACTORY ledger
763
+ * (`deps.ledger(fastify)`) must read `f.arc.modules.accounting` at bind
764
+ * time. Arc runs a module's `plugins` arm — which DOES receive the live
765
+ * instance — before ANY bootstrap, so the `extend` arm below captures it
766
+ * there and `boot` dereferences it. Registered only when the factory form
767
+ * is in play; the timing of the factory CALL is unchanged (still at
768
+ * bootstrap, still after accounting's own bootstrap via `dependsOn`).
769
+ */
770
+ let app;
771
+ const { module } = defineEngineModule({
772
+ name: "assets",
773
+ ...deps.engine !== void 0 ? { supplied: deps.engine } : {},
774
+ ...dependsOn.length > 0 ? { dependsOn } : {},
775
+ /**
776
+ * Deferred by construction (the old hand-rolled dance said "NOT
777
+ * `ownedEngine()`" for the same two reasons): `defineAssets().bind()` is
778
+ * async, and a FACTORY ledger needs the live Fastify instance — so
779
+ * nothing here may run at composition. `defineEngineModule` runs this at
780
+ * arc's bootstrap, after `dependsOn` modules.
781
+ */
782
+ boot: async () => {
783
+ let ledger;
784
+ if (typeof deps.ledger === "function") {
785
+ if (app === void 0) throw new Error("[spinekit/assets] a FACTORY ledger (`deps.ledger(fastify)`) is resolved with the live Fastify instance this module captures in its plugins phase — boot() ran without it, i.e. outside arc's lifecycle. Boot through arc, or resolve the LedgerPort yourself and pass it as a value.");
786
+ ledger = deps.ledger(app);
787
+ } else ledger = deps.ledger;
788
+ return defineAssets({
789
+ tenant: resolveSpineTenant(deps.tenant ?? false).input,
790
+ ...deps.schemaOptions ? { schemaOptions: deps.schemaOptions } : {},
791
+ ...deps.forceRecreate !== void 0 ? { forceRecreate: deps.forceRecreate } : {}
792
+ }).bind(deps.connection, {
793
+ ...ledger ? { bridges: { ledger } } : {},
794
+ ...deps.eventTransport ? { eventTransport: deps.eventTransport } : {},
795
+ ...deps.outbox ? { outbox: deps.outbox } : {},
796
+ ...deps.allowNonTransactional !== void 0 ? { allowNonTransactional: deps.allowNonTransactional } : {}
797
+ });
798
+ },
799
+ close: (engine) => engine.close(),
800
+ resources: (engine) => [createAssetResource({
801
+ engine,
802
+ permissions: deps.permissions,
803
+ currency: deps.currency,
804
+ ...deps.categoryEnum ? { categoryEnum: deps.categoryEnum } : {},
805
+ ...deps.prefix ? { prefix: deps.prefix } : {}
806
+ })],
807
+ extend: (get) => ({
808
+ ...typeof deps.ledger === "function" ? { plugins: (fastify) => {
809
+ app = fastify;
810
+ } } : {},
811
+ ...maintenance === false ? {} : { scheduledJobs: () => assetsMaintenanceSchedules(get(), maintenance) }
812
+ })
813
+ });
814
+ return module;
815
+ }
816
+ //#endregion
817
+ export { assetsMaintenanceSchedules, createAssetResource, createAssetsModule };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@spinekit/assets",
3
+ "version": "0.1.1",
4
+ "description": "Arc module for @classytic/assets — fixed-asset register + multi-book depreciation + IAS 16/36 valuation as composable resources. BYO-engine or module-created; ledger bridge is host-injected (any chart of accounts). Depreciation posts through the bridge; GL account mapping stays host policy.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "default": "./dist/index.mjs"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "LICENSE",
17
+ "dist",
18
+ "README.md",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "peerDependencies": {
22
+ "@classytic/arc": ">=2.34.0",
23
+ "@spinekit/kit": ">=0.1.0",
24
+ "@classytic/assets": ">=0.5.0",
25
+ "@classytic/mongokit": ">=3.34.0",
26
+ "mongoose": ">=9.4.1"
27
+ },
28
+ "devDependencies": {
29
+ "@classytic/arc": "^2.34.0",
30
+ "@classytic/arc-testkit": "^0.4.0",
31
+ "@classytic/assets": ">=0.5.0",
32
+ "@classytic/mongokit": ">=3.34.0",
33
+ "@classytic/primitives": ">=0.23.0",
34
+ "@types/node": "^24.3.0",
35
+ "fastify": "^5.12.0",
36
+ "mongodb-memory-server": "^10.4.3",
37
+ "mongoose": "^9.7.2",
38
+ "tsdown": "^0.22.14",
39
+ "typescript": "^7.0.2",
40
+ "vitest": "^3.2.4",
41
+ "@spinekit/kit": "0.1.1"
42
+ },
43
+ "author": "Classytic",
44
+ "homepage": "https://www.npmjs.com/package/@spinekit/assets",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/classytic/spine.git",
48
+ "directory": "packages/spine-assets"
49
+ },
50
+ "keywords": [
51
+ "arc",
52
+ "classytic",
53
+ "spine",
54
+ "erp",
55
+ "fixed-assets",
56
+ "depreciation",
57
+ "ias-16"
58
+ ],
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "scripts": {
63
+ "build": "tsdown",
64
+ "typecheck": "tsc --noEmit",
65
+ "test": "vitest run",
66
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
67
+ }
68
+ }