aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.expo/dev/logs/start.log99
-rw-r--r--.expo/devices.json4
-rw-r--r--.expo/prebuild/cached-packages.json4
-rw-r--r--.expo/types/router.d.ts6
-rw-r--r--.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.pngbin0 -> 22377 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.pngbin0 -> 37678 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.pngbin0 -> 3790 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.pngbin0 -> 7157 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.pngbin0 -> 11459 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.pngbin0 -> 21356 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.pngbin0 -> 36330 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.pngbin0 -> 3499 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.pngbin0 -> 6700 bytes
-rw-r--r--.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.pngbin0 -> 10745 bytes
-rw-r--r--.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_152.pngbin0 -> 2814 bytes
-rw-r--r--.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_228.pngbin0 -> 3365 bytes
-rw-r--r--.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_76.pngbin0 -> 1223 bytes
-rw-r--r--.gitignore4
-rw-r--r--api/.env.example4
-rw-r--r--api/.gitignore9
-rw-r--r--api/bun.lock398
-rw-r--r--api/drizzle.config.ts22
-rw-r--r--api/package.json30
-rw-r--r--api/src/db/client.ts23
-rw-r--r--api/src/db/schema/incomes.ts51
-rw-r--r--api/src/db/schema/index.ts11
-rw-r--r--api/src/db/schema/one-time-expenses.ts53
-rw-r--r--api/src/db/schema/recurring-expenses.ts69
-rw-r--r--api/src/db/schema/users.ts37
-rw-r--r--api/src/modules/auth/auth.routes.ts40
-rw-r--r--api/src/modules/auth/auth.schema.ts16
-rw-r--r--api/src/modules/auth/auth.service.ts41
-rw-r--r--api/src/modules/dashboard/dashboard.routes.ts50
-rw-r--r--api/src/modules/dashboard/dashboard.service.ts177
-rw-r--r--api/src/modules/incomes/incomes.routes.ts97
-rw-r--r--api/src/modules/incomes/incomes.schema.ts38
-rw-r--r--api/src/modules/incomes/incomes.service.ts83
-rw-r--r--api/src/modules/one-time-expenses/one-time-expenses.routes.ts97
-rw-r--r--api/src/modules/one-time-expenses/one-time-expenses.schema.ts54
-rw-r--r--api/src/modules/one-time-expenses/one-time-expenses.service.ts102
-rw-r--r--api/src/modules/recurring-expenses/recurring-expenses.routes.ts108
-rw-r--r--api/src/modules/recurring-expenses/recurring-expenses.schema.ts49
-rw-r--r--api/src/modules/recurring-expenses/recurring-expenses.service.ts115
-rw-r--r--api/src/modules/users/users.routes.ts73
-rw-r--r--api/src/modules/users/users.schema.ts23
-rw-r--r--api/src/modules/users/users.service.ts53
-rw-r--r--api/src/plugins/cors.ts18
-rw-r--r--api/src/server.ts89
-rw-r--r--api/src/utils/tax-calculator.ts111
-rw-r--r--api/tsconfig.json17
-rw-r--r--app.json23
-rw-r--r--assets/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--bun.lock3
-rw-r--r--eas.json10
-rw-r--r--package.json1
-rw-r--r--src/app/login.tsx4
-rw-r--r--src/app/register.tsx2
-rw-r--r--src/contexts/AuthContext.tsx1
-rw-r--r--src/services/api.ts1
59 files changed, 2306 insertions, 14 deletions
diff --git a/.expo/dev/logs/start.log b/.expo/dev/logs/start.log
index a7b78f4..965bde5 100644
--- a/.expo/dev/logs/start.log
+++ b/.expo/dev/logs/start.log
@@ -423,3 +423,102 @@
{"_e":"metro:bundling:started","_t":1786730404864,"id":"f","platform":"android","environment":null,"entry":"node_modules/expo-router/entry.js"}
{"_e":"metro:bundling:progress","_t":1786730404865,"id":"f","progress":0,"total":1,"current":0}
{"_e":"metro:bundling:done","_t":1786730404939,"id":"f","total":1,"ms":73.694709}
+{"_e":"root:init","_t":1786732808125,"format":"v0-jsonl","version":"57.0.14"}
+{"_e":"devserver:start","_t":1786732808138,"mode":"development","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../src/app","serverComponents":false,"serverActions":false,"serverRendering":true,"apiRoutes":false,"exporting":false}
+{"_e":"metro:config","_t":1786732808338,"serverRoot":"../../..","projectRoot":"../../..","exporting":false,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":true,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
+{"_e":"expo:dev-tools-plugin:load","_t":1786732808453,"plugins":[]}
+{"_e":"metro:instantiate","_t":1786732809368,"atlas":false,"workers":4,"host":"::","port":8081}
+{"_e":"metro:bundling:started","_t":1786732833607,"id":"1","platform":"android","environment":null,"entry":"node_modules/expo-router/entry.js"}
+{"_e":"metro:bundling:progress","_t":1786732833607,"id":"1","progress":0,"total":1,"current":0}
+{"_e":"metro:bundling:progress","_t":1786732833614,"id":"1","progress":0,"total":1,"current":0}
+{"_e":"metro:bundling:progress","_t":1786732833954,"id":"1","progress":0.010000000000000002,"total":1,"current":1}
+{"_e":"metro:bundling:progress","_t":1786732834056,"id":"1","progress":0.13573407202216065,"total":19,"current":7}
+{"_e":"metro:bundling:progress","_t":1786732834157,"id":"1","progress":0.13573407202216065,"total":33,"current":10}
+{"_e":"metro:bundling:progress","_t":1786732834258,"id":"1","progress":0.140625,"total":40,"current":15}
+{"_e":"metro:bundling:progress","_t":1786732834358,"id":"1","progress":0.18367346938775508,"total":56,"current":24}
+{"_e":"metro:bundling:progress","_t":1786732834458,"id":"1","progress":0.18566990547954262,"total":123,"current":53}
+{"_e":"metro:bundling:progress","_t":1786732834558,"id":"1","progress":0.18566990547954262,"total":256,"current":82}
+{"_e":"metro:bundling:progress","_t":1786732834659,"id":"1","progress":0.18566990547954262,"total":314,"current":108}
+{"_e":"metro:bundling:progress","_t":1786732834760,"id":"1","progress":0.18566990547954262,"total":318,"current":114}
+{"_e":"metro:bundling:progress","_t":1786732834863,"id":"1","progress":0.18566990547954262,"total":338,"current":135}
+{"_e":"metro:bundling:progress","_t":1786732834964,"id":"1","progress":0.18566990547954262,"total":379,"current":147}
+{"_e":"metro:bundling:progress","_t":1786732835064,"id":"1","progress":0.18566990547954262,"total":397,"current":170}
+{"_e":"metro:bundling:progress","_t":1786732835167,"id":"1","progress":0.2431666105546671,"total":436,"current":215}
+{"_e":"metro:bundling:progress","_t":1786732835267,"id":"1","progress":0.2843013817310301,"total":497,"current":265}
+{"_e":"metro:bundling:progress","_t":1786732835367,"id":"1","progress":0.3070317721096012,"total":536,"current":297}
+{"_e":"metro:bundling:progress","_t":1786732835496,"id":"1","progress":0.3198894851248952,"total":587,"current":332}
+{"_e":"metro:bundling:progress","_t":1786732835597,"id":"1","progress":0.34995757691974294,"total":617,"current":365}
+{"_e":"metro:bundling:progress","_t":1786732835696,"id":"1","progress":0.34995757691974294,"total":642,"current":372}
+{"_e":"metro:bundling:progress","_t":1786732835812,"id":"1","progress":0.34995757691974294,"total":650,"current":375}
+{"_e":"metro:bundling:progress","_t":1786732835913,"id":"1","progress":0.3589277742364645,"total":671,"current":402}
+{"_e":"metro:bundling:progress","_t":1786732836012,"id":"1","progress":0.40021326895025405,"total":705,"current":446}
+{"_e":"metro:bundling:progress","_t":1786732836112,"id":"1","progress":0.40021326895025405,"total":780,"current":486}
+{"_e":"metro:bundling:progress","_t":1786732836212,"id":"1","progress":0.44222500000000003,"total":800,"current":532}
+{"_e":"metro:bundling:progress","_t":1786732836317,"id":"1","progress":0.4786415069706442,"total":821,"current":568}
+{"_e":"metro:bundling:progress","_t":1786732836418,"id":"1","progress":0.5000878450501899,"total":823,"current":582}
+{"_e":"metro:bundling:progress","_t":1786732836522,"id":"1","progress":0.5217978573409764,"total":832,"current":601}
+{"_e":"metro:bundling:progress","_t":1786732836627,"id":"1","progress":0.556369255576458,"total":854,"current":637}
+{"_e":"metro:bundling:progress","_t":1786732836728,"id":"1","progress":0.556369255576458,"total":904,"current":648}
+{"_e":"metro:bundling:progress","_t":1786732836829,"id":"1","progress":0.5572758811252468,"total":931,"current":695}
+{"_e":"metro:bundling:progress","_t":1786732836928,"id":"1","progress":0.5678589353785657,"total":982,"current":740}
+{"_e":"metro:bundling:progress","_t":1786732837028,"id":"1","progress":0.5804988662131518,"total":1029,"current":784}
+{"_e":"metro:bundling:progress","_t":1786732837128,"id":"1","progress":0.6072039560328013,"total":1069,"current":833}
+{"_e":"metro:bundling:progress","_t":1786732837228,"id":"1","progress":0.6072039560328013,"total":1137,"current":870}
+{"_e":"metro:bundling:progress","_t":1786732837330,"id":"1","progress":0.6072039560328013,"total":1170,"current":884}
+{"_e":"metro:bundling:progress","_t":1786732837437,"id":"1","progress":0.6072039560328013,"total":1186,"current":899}
+{"_e":"metro:bundling:progress","_t":1786732837537,"id":"1","progress":0.6072039560328013,"total":1226,"current":923}
+{"_e":"metro:bundling:progress","_t":1786732837637,"id":"1","progress":0.6072039560328013,"total":1236,"current":958}
+{"_e":"metro:bundling:progress","_t":1786732837737,"id":"1","progress":0.6555775234131114,"total":1240,"current":1004}
+{"_e":"metro:bundling:progress","_t":1786732837837,"id":"1","progress":0.6865306122448981,"total":1260,"current":1044}
+{"_e":"metro:bundling:progress","_t":1786732837938,"id":"1","progress":0.697471755887667,"total":1286,"current":1074}
+{"_e":"metro:bundling:progress","_t":1786732838038,"id":"1","progress":0.7320908897737457,"total":1316,"current":1126}
+{"_e":"metro:bundling:progress","_t":1786732838138,"id":"1","progress":0.7320908897737457,"total":1338,"current":1136}
+{"_e":"metro:bundling:progress","_t":1786732838238,"id":"1","progress":0.7320908897737457,"total":1371,"current":1157}
+{"_e":"metro:bundling:progress","_t":1786732838339,"id":"1","progress":0.7320908897737457,"total":1394,"current":1177}
+{"_e":"metro:bundling:progress","_t":1786732838439,"id":"1","progress":0.7631270408163265,"total":1400,"current":1223}
+{"_e":"metro:bundling:progress","_t":1786732838540,"id":"1","progress":0.7920748944161405,"total":1418,"current":1262}
+{"_e":"metro:bundling:progress","_t":1786732838642,"id":"1","progress":0.8136570225573336,"total":1429,"current":1289}
+{"_e":"metro:bundling:progress","_t":1786732838750,"id":"1","progress":0.8136570225573336,"total":1513,"current":1321}
+{"_e":"metro:bundling:progress","_t":1786732838850,"id":"1","progress":0.8136570225573336,"total":1519,"current":1336}
+{"_e":"metro:bundling:progress","_t":1786732838952,"id":"1","progress":0.8136570225573336,"total":1539,"current":1353}
+{"_e":"metro:bundling:progress","_t":1786732839066,"id":"1","progress":0.8136570225573336,"total":1572,"current":1372}
+{"_e":"metro:bundling:progress","_t":1786732839166,"id":"1","progress":0.8136570225573336,"total":1573,"current":1377}
+{"_e":"metro:bundling:progress","_t":1786732839266,"id":"1","progress":0.8136570225573336,"total":1573,"current":1386}
+{"_e":"metro:bundling:progress","_t":1786732839366,"id":"1","progress":0.8136570225573336,"total":1579,"current":1405}
+{"_e":"metro:bundling:progress","_t":1786732839467,"id":"1","progress":0.8268598142757296,"total":1599,"current":1454}
+{"_e":"metro:bundling:progress","_t":1786732839566,"id":"1","progress":0.859855667371347,"total":1609,"current":1492}
+{"_e":"metro:bundling:progress","_t":1786732839667,"id":"1","progress":0.8858816690765611,"total":1616,"current":1521}
+{"_e":"root:init","_t":1786735236143,"format":"v0-jsonl","version":"57.0.14"}
+{"_e":"devserver:start","_t":1786735236152,"mode":"development","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../src/app","serverComponents":false,"serverActions":false,"serverRendering":true,"apiRoutes":false,"exporting":false}
+{"_e":"metro:config","_t":1786735236347,"serverRoot":"../../..","projectRoot":"../../..","exporting":false,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":true,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
+{"_e":"expo:dev-tools-plugin:load","_t":1786735236441,"plugins":[]}
+{"_e":"metro:instantiate","_t":1786735237108,"atlas":false,"workers":4,"host":"::","port":8081}
+{"_e":"metro:bundling:started","_t":1786735269627,"id":"1","platform":"android","environment":null,"entry":"node_modules/expo-router/entry.js"}
+{"_e":"metro:bundling:progress","_t":1786735269628,"id":"1","progress":0,"total":1,"current":0}
+{"_e":"metro:bundling:progress","_t":1786735269631,"id":"1","progress":0,"total":1,"current":0}
+{"_e":"metro:bundling:progress","_t":1786735269731,"id":"1","progress":0.19753086419753085,"total":441,"current":196}
+{"_e":"metro:bundling:progress","_t":1786735269832,"id":"1","progress":0.44940145442128326,"total":989,"current":663}
+{"_e":"metro:bundling:progress","_t":1786735269932,"id":"1","progress":0.7629691585074808,"total":1399,"current":1222}
+{"_e":"metro:bundling:progress","_t":1786735270032,"id":"1","progress":0.8858816690765611,"total":1616,"current":1521}
+{"_e":"metro:bundling:progress","_t":1786735270453,"id":"1","progress":0.8870469194196646,"total":1616,"current":1522}
+{"_e":"metro:bundling:progress","_t":1786735270554,"id":"1","progress":0.8870469194196646,"total":1620,"current":1524}
+{"_e":"metro:bundling:progress","_t":1786735270574,"id":"1","progress":0.8870469194196646,"total":1620,"current":1525}
+{"_e":"metro:bundling:progress","_t":1786735270678,"id":"1","progress":0.8870469194196646,"total":1625,"current":1528}
+{"_e":"metro:bundling:progress","_t":1786735270790,"id":"1","progress":0.8872350266916148,"total":1636,"current":1541}
+{"_e":"metro:bundling:progress","_t":1786735270890,"id":"1","progress":0.8925488339823395,"total":1647,"current":1556}
+{"_e":"metro:bundling:progress","_t":1786735270990,"id":"1","progress":0.9005975428084181,"total":1647,"current":1563}
+{"_e":"metro:bundling:progress","_t":1786735271091,"id":"1","progress":0.913318506279379,"total":1647,"current":1574}
+{"_e":"metro:bundling:progress","_t":1786735271192,"id":"1","progress":0.913318506279379,"total":1675,"current":1596}
+{"_e":"metro:bundling:progress","_t":1786735271296,"id":"1","progress":0.9163466179269343,"total":1708,"current":1635}
+{"_e":"metro:bundling:progress","_t":1786735271396,"id":"1","progress":0.931230536439669,"total":1743,"current":1682}
+{"_e":"metro:bundling:progress","_t":1786735271496,"id":"1","progress":0.931230536439669,"total":1852,"current":1718}
+{"_e":"metro:bundling:progress","_t":1786735271597,"id":"1","progress":0.931230536439669,"total":1941,"current":1767}
+{"_e":"metro:bundling:progress","_t":1786735271697,"id":"1","progress":0.931230536439669,"total":1963,"current":1825}
+{"_e":"metro:bundling:progress","_t":1786735271799,"id":"1","progress":0.931230536439669,"total":1968,"current":1860}
+{"_e":"metro:bundling:progress","_t":1786735271899,"id":"1","progress":0.931230536439669,"total":1971,"current":1864}
+{"_e":"metro:bundling:progress","_t":1786735271934,"id":"1","progress":0.931230536439669,"total":1971,"current":1865}
+{"_e":"metro:bundling:progress","_t":1786735272034,"id":"1","progress":0.9383542495663708,"total":1980,"current":1918}
+{"_e":"metro:bundling:progress","_t":1786735272136,"id":"1","progress":0.9592589736455863,"total":1992,"current":1951}
+{"_e":"metro:bundling:progress","_t":1786735272236,"id":"1","progress":0.9880538652829541,"total":2003,"current":1991}
+{"_e":"metro:bundling:progress","_t":1786735272337,"id":"1","progress":0.999,"total":2004,"current":2004}
+{"_e":"metro:bundling:done","_t":1786735272589,"id":"1","total":2004,"ms":2961.243125}
diff --git a/.expo/devices.json b/.expo/devices.json
index 571b1da..b39b60a 100644
--- a/.expo/devices.json
+++ b/.expo/devices.json
@@ -1,6 +1,10 @@
{
"devices": [
{
+ "installationId": "fa243d0b-7e85-4203-adb9-0d0bfa0ea281",
+ "lastUsed": 1786735269450
+ },
+ {
"installationId": "f06b0747-5dd9-402d-ad65-d21673fd02e8",
"lastUsed": 1786730404334
}
diff --git a/.expo/prebuild/cached-packages.json b/.expo/prebuild/cached-packages.json
new file mode 100644
index 0000000..1cd0737
--- /dev/null
+++ b/.expo/prebuild/cached-packages.json
@@ -0,0 +1,4 @@
+{
+ "dependencies": "38dd0936b63efc0bda4ddde42e3a5ac4c932c175",
+ "devDependencies": "bb8fe344f947319e69cdf214593cc09e6a829d57"
+}
diff --git a/.expo/types/router.d.ts b/.expo/types/router.d.ts
index fe9ff7f..b43aaf4 100644
--- a/.expo/types/router.d.ts
+++ b/.expo/types/router.d.ts
@@ -6,9 +6,9 @@ export * from 'expo-router';
declare module 'expo-router' {
export namespace ExpoRouter {
export interface __routes<T extends string | object = string> {
- hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/biometrics`; params?: Router.UnknownInputParams; } | { pathname: `/login`; params?: Router.UnknownInputParams; } | { pathname: `/register`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}` | `/`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/salary` | `/salary`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/recurring` | `/recurring`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/expenses` | `/expenses`; params?: Router.UnknownInputParams; };
- hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/biometrics`; params?: Router.UnknownOutputParams; } | { pathname: `/login`; params?: Router.UnknownOutputParams; } | { pathname: `/register`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}` | `/`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/salary` | `/salary`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/recurring` | `/recurring`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/expenses` | `/expenses`; params?: Router.UnknownOutputParams; };
- href: Router.RelativePathString | Router.ExternalPathString | `/biometrics${`?${string}` | `#${string}` | ''}` | `/login${`?${string}` | `#${string}` | ''}` | `/register${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}${`?${string}` | `#${string}` | ''}` | `/${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}/salary${`?${string}` | `#${string}` | ''}` | `/salary${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}/recurring${`?${string}` | `#${string}` | ''}` | `/recurring${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}/expenses${`?${string}` | `#${string}` | ''}` | `/expenses${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/biometrics`; params?: Router.UnknownInputParams; } | { pathname: `/login`; params?: Router.UnknownInputParams; } | { pathname: `/register`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}` | `/`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/salary` | `/salary`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/recurring` | `/recurring`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/expenses` | `/expenses`; params?: Router.UnknownInputParams; };
+ hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/biometrics`; params?: Router.UnknownInputParams; } | { pathname: `/login`; params?: Router.UnknownInputParams; } | { pathname: `/register`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/expenses` | `/expenses`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}` | `/`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/recurring` | `/recurring`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/salary` | `/salary`; params?: Router.UnknownInputParams; };
+ hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/biometrics`; params?: Router.UnknownOutputParams; } | { pathname: `/login`; params?: Router.UnknownOutputParams; } | { pathname: `/register`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/expenses` | `/expenses`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}` | `/`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/recurring` | `/recurring`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/salary` | `/salary`; params?: Router.UnknownOutputParams; };
+ href: Router.RelativePathString | Router.ExternalPathString | `/biometrics${`?${string}` | `#${string}` | ''}` | `/login${`?${string}` | `#${string}` | ''}` | `/register${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}/expenses${`?${string}` | `#${string}` | ''}` | `/expenses${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}${`?${string}` | `#${string}` | ''}` | `/${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}/recurring${`?${string}` | `#${string}` | ''}` | `/recurring${`?${string}` | `#${string}` | ''}` | `${'/(app)'}${'/(tabs)'}/salary${`?${string}` | `#${string}` | ''}` | `/salary${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/biometrics`; params?: Router.UnknownInputParams; } | { pathname: `/login`; params?: Router.UnknownInputParams; } | { pathname: `/register`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/expenses` | `/expenses`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}` | `/`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/recurring` | `/recurring`; params?: Router.UnknownInputParams; } | { pathname: `${'/(app)'}${'/(tabs)'}/salary` | `/salary`; params?: Router.UnknownInputParams; };
}
}
}
diff --git a/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.png b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.png
new file mode 100644
index 0000000..26ed03d
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.png b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.png
new file mode 100644
index 0000000..c538b44
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.png b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.png
new file mode 100644
index 0000000..d766b08
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.png b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.png
new file mode 100644
index 0000000..152a1ea
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.png b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.png
new file mode 100644
index 0000000..76ffc93
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-circle/android-standard-circle-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.png b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.png
new file mode 100644
index 0000000..adac4a5
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_144.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.png b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.png
new file mode 100644
index 0000000..f18ce5d
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_192.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.png b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.png
new file mode 100644
index 0000000..af1c20c
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_48.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.png b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.png
new file mode 100644
index 0000000..bac0aac
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_72.png
Binary files differ
diff --git a/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.png b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.png
new file mode 100644
index 0000000..b58ea7b
--- /dev/null
+++ b/.expo/web/cache/production/images/android-standard-square/android-standard-square-4c0282c81ca1169161c49fade57b02369065543827bc42527c087b001b76c86d-cover-transparent/icon_96.png
Binary files differ
diff --git a/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_152.png b/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_152.png
new file mode 100644
index 0000000..c0a217a
--- /dev/null
+++ b/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_152.png
Binary files differ
diff --git a/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_228.png b/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_228.png
new file mode 100644
index 0000000..c9289ca
--- /dev/null
+++ b/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_228.png
Binary files differ
diff --git a/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_76.png b/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_76.png
new file mode 100644
index 0000000..dd7546a
--- /dev/null
+++ b/.expo/web/cache/production/images/splash-ios/splash-ios-27b060a757a29038c9618586baa0ba3894dbe60d084c6059df497ff429c2d92f-contain/icon_76.png
Binary files differ
diff --git a/.gitignore b/.gitignore
index 041ad3c..1218bf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,6 @@ expo-env.d.ts
node_modules/
android/
-ios/ \ No newline at end of file
+ios/
+
+build-*.apk \ No newline at end of file
diff --git a/api/.env.example b/api/.env.example
new file mode 100644
index 0000000..8dc873f
--- /dev/null
+++ b/api/.env.example
@@ -0,0 +1,4 @@
+PORT=3333
+DATABASE_URL=yaba.db
+CORS_ORIGIN=true
+NODE_ENV=development
diff --git a/api/.gitignore b/api/.gitignore
new file mode 100644
index 0000000..2341bbb
--- /dev/null
+++ b/api/.gitignore
@@ -0,0 +1,9 @@
+node_modules
+*.db*
+.env
+
+# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
+# The following patterns were generated by expo-cli
+
+expo-env.d.ts
+# @end expo-cli \ No newline at end of file
diff --git a/api/bun.lock b/api/bun.lock
new file mode 100644
index 0000000..f4166c0
--- /dev/null
+++ b/api/bun.lock
@@ -0,0 +1,398 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "yaba-api",
+ "dependencies": {
+ "@fastify/cors": "^10.0.2",
+ "@fastify/jwt": "^10.2.1",
+ "bcryptjs": "^3.0.3",
+ "drizzle-orm": "^0.44.2",
+ "fastify": "^5.3.2",
+ "zod": "^3.25.76",
+ },
+ "devDependencies": {
+ "@libsql/client": "^0.17.4",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/bun": "^1.2.18",
+ "bun-types": "^1.2.18",
+ "drizzle-kit": "^0.31.4",
+ "typescript": "^5.8.3",
+ },
+ },
+ },
+ "packages": {
+ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
+
+ "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
+
+ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+
+ "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
+
+ "@fastify/cors": ["@fastify/cors@10.1.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "mnemonist": "0.40.0" } }, "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ=="],
+
+ "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
+
+ "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.1.0", "", { "dependencies": { "fast-json-stringify": "^7.0.0" } }, "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw=="],
+
+ "@fastify/forwarded": ["@fastify/forwarded@3.0.2", "", {}, "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA=="],
+
+ "@fastify/jwt": ["@fastify/jwt@10.2.1", "", { "dependencies": { "@fastify/error": "^4.2.0", "@lukeed/ms": "^2.0.2", "fast-jwt": "^6.2.4", "fastify-plugin": "^6.0.0", "steed": "^1.1.3" } }, "sha512-8FHalB082a6HFj+neP9TdJZrUa65Aq6mtozVjd3vH3Yy7hmjRJUDKLPdxYRYF9S077JeN9RgimsautqnijbDlQ=="],
+
+ "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
+
+ "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
+
+ "@libsql/client": ["@libsql/client@0.17.4", "", { "dependencies": { "@libsql/core": "^0.17.4", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw=="],
+
+ "@libsql/core": ["@libsql/core@0.17.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ=="],
+
+ "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="],
+
+ "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="],
+
+ "@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="],
+
+ "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="],
+
+ "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="],
+
+ "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="],
+
+ "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="],
+
+ "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="],
+
+ "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="],
+
+ "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="],
+
+ "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="],
+
+ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
+
+ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
+
+ "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
+
+ "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
+
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+
+ "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="],
+
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
+
+ "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
+
+ "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
+
+ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+
+ "asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="],
+
+ "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
+
+ "avvio": ["avvio@9.3.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A=="],
+
+ "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
+
+ "bn.js": ["bn.js@4.12.5", "", {}, "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
+
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
+
+ "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
+
+ "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+
+ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stringify": ["fast-json-stringify@7.0.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^4.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA=="],
+
+ "fast-jwt": ["fast-jwt@6.3.1", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "asn1.js": "^5.4.1", "ecdsa-sig-formatter": "^1.0.11", "mnemonist": "^0.40.0", "safe-regex2": "^5.1.0" } }, "sha512-9LFiA4oIx7CjPjep7G5zrfM+UiA+60lIsYH/6I5kCwGBa06WUtdwJ3F3m8Z79bxdUVz/lW30A/NsWWStqoJKrw=="],
+
+ "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
+
+ "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="],
+
+ "fastfall": ["fastfall@1.5.1", "", { "dependencies": { "reusify": "^1.0.0" } }, "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q=="],
+
+ "fastify": ["fastify@5.11.2", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^7.0.0", "find-my-way": "^9.6.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-i/eJG7nXR9OkbFgoX4jFiPOHoRq0rXqDACVAXELh5Fdg6BFBErIVZ8MyibGCwup9Go1pYrxZJ0ogIub8vVdEcQ=="],
+
+ "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
+
+ "fastparallel": ["fastparallel@2.4.1", "", { "dependencies": { "reusify": "^1.0.4", "xtend": "^4.0.2" } }, "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q=="],
+
+ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
+
+ "fastseries": ["fastseries@1.7.2", "", { "dependencies": { "reusify": "^1.0.0", "xtend": "^4.0.0" } }, "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ=="],
+
+ "find-my-way": ["find-my-way@9.7.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "get-tsconfig": ["get-tsconfig@4.14.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="],
+
+ "js-base64": ["js-base64@3.9.2", "", {}, "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA=="],
+
+ "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
+
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="],
+
+ "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
+
+ "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
+
+ "mnemonist": ["mnemonist@0.40.0", "", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg=="],
+
+ "obliterator": ["obliterator@2.0.5", "", {}, "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="],
+
+ "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
+
+ "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="],
+
+ "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
+
+ "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
+
+ "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="],
+
+ "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="],
+
+ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
+
+ "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
+ "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
+
+ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+
+ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safe-regex2": ["safe-regex2@5.1.1", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA=="],
+
+ "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
+
+ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
+
+ "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
+
+ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+
+ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
+
+ "steed": ["steed@1.1.3", "", { "dependencies": { "fastfall": "^1.5.0", "fastparallel": "^2.2.0", "fastq": "^1.3.0", "fastseries": "^1.7.0", "reusify": "^1.0.0" } }, "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA=="],
+
+ "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
+
+ "toad-cache": ["toad-cache@3.7.4", "", {}, "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg=="],
+
+ "tsx": ["tsx@4.23.5", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
+
+ "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
+
+ "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
+
+ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
+
+ "@fastify/jwt/fastify-plugin": ["fastify-plugin@6.0.0", "", {}, "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg=="],
+
+ "fast-json-stringify/fast-uri": ["fast-uri@4.1.2", "", {}, "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw=="],
+
+ "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
+
+ "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
+
+ "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
+
+ "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
+
+ "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
+
+ "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
+
+ "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
+
+ "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
+
+ "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
+
+ "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
+
+ "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
+
+ "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
+
+ "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
+
+ "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
+
+ "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
+
+ "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
+
+ "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
+
+ "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
+
+ "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
+
+ "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
+
+ "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
+
+ "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
+
+ "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
+
+ "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
+
+ "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
+
+ "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
+
+ "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
+
+ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
+
+ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
+ }
+}
diff --git a/api/drizzle.config.ts b/api/drizzle.config.ts
new file mode 100644
index 0000000..7ab2647
--- /dev/null
+++ b/api/drizzle.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'drizzle-kit';
+
+export default defineConfig({
+ // Localização dos arquivos de schema
+ schema: './src/db/schema/index.ts',
+
+ // Onde o Drizzle Kit vai gerar as migrations
+ out: './src/db/migrations',
+
+ // Driver SQLite
+ dialect: 'sqlite',
+
+ dbCredentials: {
+ url: process.env.DATABASE_URL ?? 'yaba.db',
+ },
+
+ // Exibe SQL das migrations no console durante o desenvolvimento
+ verbose: true,
+
+ // Impede destruição acidental de dados em produção
+ strict: true,
+});
diff --git a/api/package.json b/api/package.json
new file mode 100644
index 0000000..aa50cff
--- /dev/null
+++ b/api/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "yaba-api",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "bun run --watch src/server.ts",
+ "start": "bun run src/server.ts",
+ "db:generate": "drizzle-kit generate",
+ "db:migrate": "drizzle-kit migrate",
+ "db:studio": "drizzle-kit studio",
+ "db:push": "drizzle-kit push"
+ },
+ "dependencies": {
+ "@fastify/cors": "^10.0.2",
+ "@fastify/jwt": "^10.2.1",
+ "bcryptjs": "^3.0.3",
+ "drizzle-orm": "^0.44.2",
+ "fastify": "^5.3.2",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@libsql/client": "^0.17.4",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/bun": "^1.2.18",
+ "bun-types": "^1.2.18",
+ "drizzle-kit": "^0.31.4",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/api/src/db/client.ts b/api/src/db/client.ts
new file mode 100644
index 0000000..5e9673f
--- /dev/null
+++ b/api/src/db/client.ts
@@ -0,0 +1,23 @@
+import { Database } from 'bun:sqlite';
+import { drizzle } from 'drizzle-orm/bun-sqlite';
+import * as schema from './schema';
+
+/**
+ * Instância única do banco de dados SQLite via Drizzle ORM.
+ * Utiliza o driver nativo do Bun para máxima performance.
+ *
+ * O caminho do arquivo pode ser configurado via variável de ambiente DATABASE_URL.
+ * Quando não definida, usa 'yaba.db' na raiz do projeto.
+ */
+const sqlite = new Database(process.env.DATABASE_URL ?? 'yaba.db');
+
+/**
+ * Habilita WAL mode para melhor performance em leituras concorrentes.
+ * @see https://www.sqlite.org/wal.html
+ */
+sqlite.exec('PRAGMA journal_mode = WAL;');
+sqlite.exec('PRAGMA foreign_keys = ON;');
+
+export const db = drizzle(sqlite, { schema });
+
+export type DB = typeof db;
diff --git a/api/src/db/schema/incomes.ts b/api/src/db/schema/incomes.ts
new file mode 100644
index 0000000..f502cb9
--- /dev/null
+++ b/api/src/db/schema/incomes.ts
@@ -0,0 +1,51 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { users } from './users';
+
+/**
+ * Tabela de rendas/salários.
+ * Armazena o histórico de salários por mês/ano, permitindo que o usuário
+ * tenha registros diferentes para cada período (ex: aumento de salário em julho).
+ */
+export const incomes = sqliteTable('incomes', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ user_id: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+
+ /** Salário bruto do período (em R$) */
+ gross_salary: real('gross_salary').notNull(),
+
+ /**
+ * Salário líquido calculado automaticamente pelo app (INSS + IRRF).
+ * Armazenado para fins de histórico e auditoria.
+ */
+ calculated_net_salary: real('calculated_net_salary').notNull(),
+
+ /**
+ * Salário líquido efetivo usado no cálculo do saldo.
+ * Pode diferir do calculado caso o usuário tenha feito um override manual.
+ */
+ effective_net_salary: real('effective_net_salary').notNull(),
+
+ /** Mês de referência (1-12) */
+ reference_month: integer('reference_month').notNull(),
+
+ /** Ano de referência (ex: 2025) */
+ reference_year: integer('reference_year').notNull(),
+
+ /** Observações opcionais (ex: "13º salário", "PLR") */
+ description: text('description'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type Income = typeof incomes.$inferSelect;
+export type NewIncome = typeof incomes.$inferInsert;
diff --git a/api/src/db/schema/index.ts b/api/src/db/schema/index.ts
new file mode 100644
index 0000000..72cf261
--- /dev/null
+++ b/api/src/db/schema/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Ponto central de exportação de todos os schemas do Drizzle.
+ * Importe sempre a partir deste arquivo para evitar imports circulares.
+ *
+ * Uso:
+ * import { users, incomes, recurringExpenses, oneTimeExpenses } from '@/db/schema';
+ */
+export * from './users';
+export * from './incomes';
+export * from './recurring-expenses';
+export * from './one-time-expenses';
diff --git a/api/src/db/schema/one-time-expenses.ts b/api/src/db/schema/one-time-expenses.ts
new file mode 100644
index 0000000..10c9c01
--- /dev/null
+++ b/api/src/db/schema/one-time-expenses.ts
@@ -0,0 +1,53 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { users } from './users';
+
+/**
+ * Tabela de gastos avulsos (despesas pontuais).
+ * Ex: Cinema, iFood, Uber, Farmácia.
+ * Cada registro representa um gasto único em uma data específica.
+ * O valor é subtraído do saldo disponível do mês em que a despesa ocorreu.
+ */
+export const oneTimeExpenses = sqliteTable('one_time_expenses', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ user_id: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+
+ /** Nome/descrição do gasto (ex: "Cinema com a família", "iFood jantar") */
+ name: text('name').notNull(),
+
+ /** Valor do gasto (em R$) */
+ amount: real('amount').notNull(),
+
+ /**
+ * Categoria do gasto para organização e futuros relatórios.
+ * Ex: "alimentação", "lazer", "saúde", "transporte", "educação", "outros"
+ */
+ category: text('category').notNull().default('outros'),
+
+ /** Tipo do lançamento: 'expense' (despesa) ou 'income' (receita) */
+ type: text('type').notNull().default('expense'),
+
+ /**
+ * Data em que o gasto ocorreu.
+ * Formato ISO 8601 (YYYY-MM-DD). Usado para atribuir o gasto ao mês correto.
+ * Default: data atual no momento da inserção.
+ */
+ expense_date: text('expense_date').notNull().default(sql`(date('now'))`),
+
+ /** Observações opcionais (ex: "Split 50/50 com João") */
+ notes: text('notes'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type OneTimeExpense = typeof oneTimeExpenses.$inferSelect;
+export type NewOneTimeExpense = typeof oneTimeExpenses.$inferInsert;
diff --git a/api/src/db/schema/recurring-expenses.ts b/api/src/db/schema/recurring-expenses.ts
new file mode 100644
index 0000000..8a0a1df
--- /dev/null
+++ b/api/src/db/schema/recurring-expenses.ts
@@ -0,0 +1,69 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { users } from './users';
+
+/**
+ * Tabela de gastos recorrentes (despesas fixas mensais).
+ * Ex: Aluguel, Internet, Streaming, Academia.
+ * Estes gastos são deduzidos automaticamente do saldo todo mês,
+ * sem precisar recadastrar.
+ */
+export const recurringExpenses = sqliteTable('recurring_expenses', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ user_id: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+
+ /** Nome/descrição do gasto (ex: "Aluguel", "Netflix") */
+ name: text('name').notNull(),
+
+ /** Valor mensal do gasto (em R$) */
+ amount: real('amount').notNull(),
+
+ /**
+ * Categoria do gasto para organização e futuros relatórios.
+ * Ex: "moradia", "alimentação", "lazer", "saúde", "transporte", "educação", "outros"
+ */
+ category: text('category').notNull().default('outros'),
+
+ /** Tipo do lançamento: 'expense' (despesa) ou 'income' (receita) */
+ type: text('type').notNull().default('expense'),
+
+ /**
+ * Dia do mês em que a despesa costuma vencer (1-31).
+ * Opcional — usado apenas para informação ao usuário.
+ */
+ due_day: integer('due_day'),
+
+ /**
+ * Indica se o gasto está ativo.
+ * Quando false, não é deduzido do saldo (útil para pausar sem deletar).
+ * Armazenado como integer (0 = false, 1 = true) por limitação do SQLite.
+ */
+ is_active: integer('is_active', { mode: 'boolean' }).notNull().default(true),
+
+ /**
+ * Data a partir da qual este gasto passa a ser contabilizado.
+ * Formato ISO 8601 (YYYY-MM-DD).
+ */
+ starts_at: text('starts_at').notNull().default(sql`(date('now'))`),
+
+ /**
+ * Data em que este gasto deixa de ser contabilizado (opcional).
+ * Útil para despesas com prazo definido (ex: parcelamento de 12x).
+ * Formato ISO 8601 (YYYY-MM-DD).
+ */
+ ends_at: text('ends_at'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type RecurringExpense = typeof recurringExpenses.$inferSelect;
+export type NewRecurringExpense = typeof recurringExpenses.$inferInsert;
diff --git a/api/src/db/schema/users.ts b/api/src/db/schema/users.ts
new file mode 100644
index 0000000..636ad2d
--- /dev/null
+++ b/api/src/db/schema/users.ts
@@ -0,0 +1,37 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+
+/**
+ * Tabela de usuários.
+ * Por ora, suporta um único usuário local (single-user app).
+ * O campo `net_salary_override` permite que o usuário edite manualmente
+ * o salário líquido calculado, para cobrir descontos extras (plano de saúde, VT, etc.).
+ */
+export const users = sqliteTable('users', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ name: text('name').notNull(),
+ email: text('email').unique().notNull(),
+ password_hash: text('password_hash').notNull(),
+
+ /** Salário bruto informado pelo usuário (em R$) */
+ gross_salary: real('gross_salary').notNull().default(0),
+
+ /**
+ * Override manual do salário líquido.
+ * Quando NULL, o app usa o valor calculado automaticamente (INSS + IRRF).
+ * Quando preenchido, este valor é usado como base para o saldo disponível.
+ */
+ net_salary_override: real('net_salary_override'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type User = typeof users.$inferSelect;
+export type NewUser = typeof users.$inferInsert;
diff --git a/api/src/modules/auth/auth.routes.ts b/api/src/modules/auth/auth.routes.ts
new file mode 100644
index 0000000..d388694
--- /dev/null
+++ b/api/src/modules/auth/auth.routes.ts
@@ -0,0 +1,40 @@
+import type { FastifyPluginAsync } from 'fastify';
+import { ZodError } from 'zod';
+import { loginSchema, registerSchema } from './auth.schema';
+import * as authService from './auth.service';
+
+const authRoutes: FastifyPluginAsync = async (fastify) => {
+ fastify.post('/auth/register', async (request, reply) => {
+ try {
+ const body = registerSchema.parse(request.body);
+ const user = await authService.registerUser(body);
+
+ const token = fastify.jwt.sign({ sub: user.id });
+
+ return reply.status(201).send({ data: { user, token } });
+ } catch (err: any) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ return reply.status(400).send({ error: err.message });
+ }
+ });
+
+ fastify.post('/auth/login', async (request, reply) => {
+ try {
+ const body = loginSchema.parse(request.body);
+ const user = await authService.loginUser(body);
+
+ const token = fastify.jwt.sign({ sub: user.id });
+
+ return reply.send({ data: { user, token } });
+ } catch (err: any) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ return reply.status(401).send({ error: err.message });
+ }
+ });
+};
+
+export default authRoutes;
diff --git a/api/src/modules/auth/auth.schema.ts b/api/src/modules/auth/auth.schema.ts
new file mode 100644
index 0000000..550c670
--- /dev/null
+++ b/api/src/modules/auth/auth.schema.ts
@@ -0,0 +1,16 @@
+import { z } from 'zod';
+
+export const registerSchema = z.object({
+ name: z.string().min(2, 'Nome deve ter pelo menos 2 caracteres'),
+ email: z.string().email('Email inválido'),
+ password: z.string().min(6, 'Senha deve ter pelo menos 6 caracteres'),
+ gross_salary: z.coerce.number().min(0).optional(),
+});
+
+export const loginSchema = z.object({
+ email: z.string().email('Email inválido'),
+ password: z.string().min(6, 'Senha inválida'),
+});
+
+export type RegisterData = z.infer<typeof registerSchema>;
+export type LoginData = z.infer<typeof loginSchema>;
diff --git a/api/src/modules/auth/auth.service.ts b/api/src/modules/auth/auth.service.ts
new file mode 100644
index 0000000..c874b05
--- /dev/null
+++ b/api/src/modules/auth/auth.service.ts
@@ -0,0 +1,41 @@
+import bcrypt from 'bcryptjs';
+import { eq } from 'drizzle-orm';
+import { db } from '../../db/client';
+import { users } from '../../db/schema/users';
+import type { LoginData, RegisterData } from './auth.schema';
+
+export async function registerUser(data: RegisterData) {
+ const existingUser = await db.select().from(users).where(eq(users.email, data.email)).limit(1);
+ if (existingUser.length > 0) {
+ throw new Error('Email já está em uso.');
+ }
+
+ const salt = await bcrypt.genSalt(10);
+ const passwordHash = await bcrypt.hash(data.password, salt);
+
+ const [newUser] = await db.insert(users).values({
+ name: data.name,
+ email: data.email,
+ password_hash: passwordHash,
+ gross_salary: data.gross_salary || 0,
+ }).returning();
+
+ const { password_hash, ...userWithoutPassword } = newUser;
+ return userWithoutPassword;
+}
+
+export async function loginUser(data: LoginData) {
+ const [user] = await db.select().from(users).where(eq(users.email, data.email)).limit(1);
+
+ if (!user) {
+ throw new Error('Email ou senha inválidos.');
+ }
+
+ const isValid = await bcrypt.compare(data.password, user.password_hash);
+ if (!isValid) {
+ throw new Error('Email ou senha inválidos.');
+ }
+
+ const { password_hash, ...userWithoutPassword } = user;
+ return userWithoutPassword;
+}
diff --git a/api/src/modules/dashboard/dashboard.routes.ts b/api/src/modules/dashboard/dashboard.routes.ts
new file mode 100644
index 0000000..d579ef2
--- /dev/null
+++ b/api/src/modules/dashboard/dashboard.routes.ts
@@ -0,0 +1,50 @@
+import type { FastifyPluginAsync } from 'fastify';
+import { z } from 'zod';
+import * as dashboardService from './dashboard.service';
+
+const dashboardQuerySchema = z.object({
+ month: z.coerce.number().int().min(1).max(12).optional(),
+ year: z.coerce.number().int().min(2020).optional(),
+});
+
+const dashboardParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+});
+
+const dashboardRoutes: FastifyPluginAsync = async (fastify) => {
+ /**
+ * GET /users/:userId/dashboard
+ * Retorna o resumo financeiro completo do mês.
+ *
+ * Query params (opcionais):
+ * ?month=6&year=2025 → mês/ano específico
+ * Sem params → mês/ano atual
+ *
+ * Resposta:
+ * - Dados do usuário e salário
+ * - Lista de gastos recorrentes ativos
+ * - Lista de gastos avulsos do mês
+ * - Saldo disponível e percentual comprometido
+ */
+ fastify.get<{ Params: { userId: string }; Querystring: Record<string, string> }>(
+ '/users/:userId/dashboard',
+ async (request, reply) => {
+ const { userId } = dashboardParamsSchema.parse(request.params);
+ const query = dashboardQuerySchema.parse(request.query);
+
+ const now = new Date();
+ const month = query.month ?? now.getMonth() + 1;
+ const year = query.year ?? now.getFullYear();
+
+ const data = await dashboardService.getDashboard(userId, month, year);
+
+ if (!data) {
+ return reply.status(404).send({ error: 'Usuário não encontrado.' });
+ }
+
+ return reply.send({ data });
+ },
+ );
+};
+
+export default dashboardRoutes;
diff --git a/api/src/modules/dashboard/dashboard.service.ts b/api/src/modules/dashboard/dashboard.service.ts
new file mode 100644
index 0000000..78b1d1b
--- /dev/null
+++ b/api/src/modules/dashboard/dashboard.service.ts
@@ -0,0 +1,177 @@
+import { and, eq, gte, lte, sql } from 'drizzle-orm';
+import { db } from '@/db/client';
+import { incomes, oneTimeExpenses, recurringExpenses, users } from '@/db/schema';
+import { calcularSalarioLiquido } from '@/utils/tax-calculator';
+
+export interface DashboardData {
+ user: {
+ id: number;
+ name: string;
+ gross_salary: number;
+ net_salary_override: number | null;
+ };
+ period: {
+ month: number;
+ year: number;
+ };
+ salary: {
+ gross: number;
+ calculated_net: number;
+ effective_net: number;
+ inss_discount: number;
+ irrf_discount: number;
+ has_override: boolean;
+ };
+ expenses: {
+ recurring_total: number;
+ one_time_total: number;
+ total: number;
+ recurring_list: { id: number; name: string; amount: number; category: string }[];
+ one_time_list: {
+ id: number;
+ name: string;
+ amount: number;
+ category: string;
+ expense_date: string;
+ }[];
+ };
+ balance: {
+ available: number;
+ /** Percentual do salário líquido comprometido com despesas (0-100) */
+ expense_ratio: number;
+ };
+}
+
+export async function getDashboard(
+ userId: number,
+ referenceMonth: number,
+ referenceYear: number,
+): Promise<DashboardData | null> {
+ // 1. Busca o usuário
+ const [user] = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, userId))
+ .limit(1);
+
+ if (!user) return null;
+
+ // 2. Calcula o salário via tabela INSS/IRRF
+ const taxResult = calcularSalarioLiquido(user.gross_salary);
+
+ // 3. Salário efetivo: usa override do usuário ou o calculado
+ const effectiveNet = user.net_salary_override ?? taxResult.salarioLiquidoCalculado;
+
+ // 4. Tenta encontrar um registro de renda para o mês/ano pedido
+ const [incomeRecord] = await db
+ .select()
+ .from(incomes)
+ .where(
+ and(
+ eq(incomes.user_id, userId),
+ eq(incomes.reference_month, referenceMonth),
+ eq(incomes.reference_year, referenceYear),
+ ),
+ )
+ .limit(1);
+
+ // Se existir registro de renda para o mês, usa os valores dele
+ const salaryData = incomeRecord
+ ? {
+ gross: incomeRecord.gross_salary,
+ calculated_net: incomeRecord.calculated_net_salary,
+ effective_net: incomeRecord.effective_net_salary,
+ inss_discount:
+ incomeRecord.gross_salary - incomeRecord.calculated_net_salary - 0, // simplificado
+ irrf_discount: 0,
+ has_override: incomeRecord.effective_net_salary !== incomeRecord.calculated_net_salary,
+ }
+ : {
+ gross: user.gross_salary,
+ calculated_net: taxResult.salarioLiquidoCalculado,
+ effective_net: effectiveNet,
+ inss_discount: taxResult.descontoINSS,
+ irrf_discount: taxResult.descontoIRRF,
+ has_override: user.net_salary_override !== null,
+ };
+
+ // 5. Busca gastos recorrentes ATIVOS e vigentes no mês/ano pedido
+ const refDateStr = `${referenceYear}-${String(referenceMonth).padStart(2, '0')}-01`;
+ const lastDay = new Date(referenceYear, referenceMonth, 0).getDate();
+ const refDateEndStr = `${referenceYear}-${String(referenceMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
+
+ const recurringList = await db
+ .select()
+ .from(recurringExpenses)
+ .where(
+ and(
+ eq(recurringExpenses.user_id, userId),
+ eq(recurringExpenses.is_active, true),
+ lte(recurringExpenses.starts_at, refDateEndStr),
+ // ends_at IS NULL ou ends_at >= início do mês
+ sql`(${recurringExpenses.ends_at} IS NULL OR ${recurringExpenses.ends_at} >= ${refDateStr})`,
+ ),
+ );
+
+ // 6. Busca gastos avulsos do mês/ano pedido
+ const oneTimeList = await db
+ .select()
+ .from(oneTimeExpenses)
+ .where(
+ and(
+ eq(oneTimeExpenses.user_id, userId),
+ gte(oneTimeExpenses.expense_date, refDateStr),
+ lte(oneTimeExpenses.expense_date, refDateEndStr),
+ ),
+ )
+ .orderBy(oneTimeExpenses.expense_date);
+
+ // 7. Calcula totais
+ const recurringTotal = recurringList.reduce((sum, e) => {
+ return sum + (e.type === 'income' ? -e.amount : e.amount);
+ }, 0);
+ const oneTimeTotal = oneTimeList.reduce((sum, e) => {
+ return sum + (e.type === 'income' ? -e.amount : e.amount);
+ }, 0);
+ const totalExpenses = parseFloat((recurringTotal + oneTimeTotal).toFixed(2));
+ const available = parseFloat((salaryData.effective_net - totalExpenses).toFixed(2));
+ const expenseRatio =
+ salaryData.effective_net > 0
+ ? parseFloat(((totalExpenses / salaryData.effective_net) * 100).toFixed(1))
+ : 0;
+
+ return {
+ user: {
+ id: user.id,
+ name: user.name,
+ gross_salary: user.gross_salary,
+ net_salary_override: user.net_salary_override,
+ },
+ period: { month: referenceMonth, year: referenceYear },
+ salary: salaryData,
+ expenses: {
+ recurring_total: parseFloat(recurringTotal.toFixed(2)),
+ one_time_total: parseFloat(oneTimeTotal.toFixed(2)),
+ total: totalExpenses,
+ recurring_list: recurringList.map((e) => ({
+ id: e.id,
+ name: e.name,
+ amount: e.amount,
+ category: e.category,
+ type: e.type,
+ })),
+ one_time_list: oneTimeList.map((e) => ({
+ id: e.id,
+ name: e.name,
+ amount: e.amount,
+ category: e.category,
+ type: e.type,
+ expense_date: e.expense_date,
+ })),
+ },
+ balance: {
+ available,
+ expense_ratio: expenseRatio,
+ },
+ };
+}
diff --git a/api/src/modules/incomes/incomes.routes.ts b/api/src/modules/incomes/incomes.routes.ts
new file mode 100644
index 0000000..7932d18
--- /dev/null
+++ b/api/src/modules/incomes/incomes.routes.ts
@@ -0,0 +1,97 @@
+import type { FastifyPluginAsync } from 'fastify';
+import { ZodError } from 'zod';
+import {
+ createIncomeSchema,
+ incomeParamsSchema,
+ incomeQuerySchema,
+ incomeUserParamsSchema,
+ updateIncomeSchema,
+} from './incomes.schema';
+import * as incomesService from './incomes.service';
+
+const incomesRoutes: FastifyPluginAsync = async (fastify) => {
+ /**
+ * GET /users/:userId/incomes
+ * Lista todas as rendas de um usuário.
+ * Query params: ?month=6&year=2025
+ */
+ fastify.get<{ Params: { userId: string }; Querystring: Record<string, string> }>(
+ '/users/:userId/incomes',
+ async (request, reply) => {
+ const { userId } = incomeUserParamsSchema.parse(request.params);
+ const query = incomeQuerySchema.parse(request.query);
+ const result = await incomesService.findIncomesByUser(userId, query);
+ return reply.send({ data: result });
+ },
+ );
+
+ /** GET /users/:userId/incomes/:id — Busca uma renda específica */
+ fastify.get<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/incomes/:id',
+ async (request, reply) => {
+ const { userId, id } = incomeParamsSchema.parse(request.params);
+ const income = await incomesService.findIncomeById(userId, id);
+
+ if (!income) {
+ return reply.status(404).send({ error: 'Renda não encontrada.' });
+ }
+ return reply.send({ data: income });
+ },
+ );
+
+ /** POST /users/:userId/incomes — Registra a renda de um mês */
+ fastify.post<{ Params: { userId: string } }>(
+ '/users/:userId/incomes',
+ async (request, reply) => {
+ try {
+ const { userId } = incomeUserParamsSchema.parse(request.params);
+ const body = createIncomeSchema.parse(request.body);
+ const income = await incomesService.createIncome(userId, body);
+ return reply.status(201).send({ data: income });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ },
+ );
+
+ /** PUT /users/:userId/incomes/:id — Atualiza uma renda */
+ fastify.put<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/incomes/:id',
+ async (request, reply) => {
+ try {
+ const { userId, id } = incomeParamsSchema.parse(request.params);
+ const body = updateIncomeSchema.parse(request.body);
+ const income = await incomesService.updateIncome(userId, id, body);
+
+ if (!income) {
+ return reply.status(404).send({ error: 'Renda não encontrada.' });
+ }
+ return reply.send({ data: income });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ },
+ );
+
+ /** DELETE /users/:userId/incomes/:id — Remove uma renda */
+ fastify.delete<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/incomes/:id',
+ async (request, reply) => {
+ const { userId, id } = incomeParamsSchema.parse(request.params);
+ const deleted = await incomesService.deleteIncome(userId, id);
+
+ if (!deleted) {
+ return reply.status(404).send({ error: 'Renda não encontrada.' });
+ }
+ return reply.status(200).send({ data: deleted, message: 'Renda removida com sucesso.' });
+ },
+ );
+};
+
+export default incomesRoutes;
diff --git a/api/src/modules/incomes/incomes.schema.ts b/api/src/modules/incomes/incomes.schema.ts
new file mode 100644
index 0000000..cc661fd
--- /dev/null
+++ b/api/src/modules/incomes/incomes.schema.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+
+export const createIncomeSchema = z.object({
+ gross_salary: z.number().positive('Salário bruto deve ser positivo'),
+ /**
+ * Salário líquido efetivo (após override do usuário).
+ * Deve ser enviado pelo app após o cálculo de INSS/IRRF.
+ */
+ calculated_net_salary: z.number().nonnegative(),
+ effective_net_salary: z.number().nonnegative(),
+ reference_month: z.number().int().min(1).max(12),
+ reference_year: z.number().int().min(2020).max(2100),
+ description: z.string().max(255).nullable().optional(),
+});
+
+export const updateIncomeSchema = z.object({
+ gross_salary: z.number().positive().optional(),
+ calculated_net_salary: z.number().nonnegative().optional(),
+ effective_net_salary: z.number().nonnegative().optional(),
+ description: z.string().max(255).nullable().optional(),
+});
+
+export const incomeParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+ id: z.coerce.number().int().positive(),
+});
+
+export const incomeUserParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+});
+
+export const incomeQuerySchema = z.object({
+ month: z.coerce.number().int().min(1).max(12).optional(),
+ year: z.coerce.number().int().min(2020).optional(),
+});
+
+export type CreateIncomeInput = z.infer<typeof createIncomeSchema>;
+export type UpdateIncomeInput = z.infer<typeof updateIncomeSchema>;
diff --git a/api/src/modules/incomes/incomes.service.ts b/api/src/modules/incomes/incomes.service.ts
new file mode 100644
index 0000000..0a9ea84
--- /dev/null
+++ b/api/src/modules/incomes/incomes.service.ts
@@ -0,0 +1,83 @@
+import { and, eq } from 'drizzle-orm';
+import { db } from '@/db/client';
+import { incomes } from '@/db/schema';
+import type { CreateIncomeInput, UpdateIncomeInput } from './incomes.schema';
+
+export async function findIncomesByUser(
+ userId: number,
+ filters?: { month?: number; year?: number },
+) {
+ const conditions = [eq(incomes.user_id, userId)];
+
+ if (filters?.month !== undefined) {
+ conditions.push(eq(incomes.reference_month, filters.month));
+ }
+ if (filters?.year !== undefined) {
+ conditions.push(eq(incomes.reference_year, filters.year));
+ }
+
+ return db
+ .select()
+ .from(incomes)
+ .where(and(...conditions))
+ .orderBy(incomes.reference_year, incomes.reference_month);
+}
+
+export async function findIncomeById(userId: number, id: number) {
+ const result = await db
+ .select()
+ .from(incomes)
+ .where(and(eq(incomes.user_id, userId), eq(incomes.id, id)))
+ .limit(1);
+ return result[0] ?? null;
+}
+
+export async function createIncome(userId: number, data: CreateIncomeInput) {
+ const [created] = await db
+ .insert(incomes)
+ .values({
+ user_id: userId,
+ gross_salary: data.gross_salary,
+ calculated_net_salary: data.calculated_net_salary,
+ effective_net_salary: data.effective_net_salary,
+ reference_month: data.reference_month,
+ reference_year: data.reference_year,
+ description: data.description ?? null,
+ })
+ .returning();
+ return created;
+}
+
+export async function updateIncome(
+ userId: number,
+ id: number,
+ data: UpdateIncomeInput,
+) {
+ const now = new Date().toISOString();
+
+ const [updated] = await db
+ .update(incomes)
+ .set({
+ ...(data.gross_salary !== undefined && { gross_salary: data.gross_salary }),
+ ...(data.calculated_net_salary !== undefined && {
+ calculated_net_salary: data.calculated_net_salary,
+ }),
+ ...(data.effective_net_salary !== undefined && {
+ effective_net_salary: data.effective_net_salary,
+ }),
+ ...(data.description !== undefined && { description: data.description }),
+ updated_at: now,
+ })
+ .where(and(eq(incomes.user_id, userId), eq(incomes.id, id)))
+ .returning();
+
+ return updated ?? null;
+}
+
+export async function deleteIncome(userId: number, id: number) {
+ const [deleted] = await db
+ .delete(incomes)
+ .where(and(eq(incomes.user_id, userId), eq(incomes.id, id)))
+ .returning();
+ return deleted ?? null;
+}
diff --git a/api/src/modules/one-time-expenses/one-time-expenses.routes.ts b/api/src/modules/one-time-expenses/one-time-expenses.routes.ts
new file mode 100644
index 0000000..0658cfc
--- /dev/null
+++ b/api/src/modules/one-time-expenses/one-time-expenses.routes.ts
@@ -0,0 +1,97 @@
+import type { FastifyPluginAsync } from 'fastify';
+import { ZodError } from 'zod';
+import {
+ createOneTimeExpenseSchema,
+ oneTimeExpenseParamsSchema,
+ oneTimeExpenseQuerySchema,
+ oneTimeExpenseUserParamsSchema,
+ updateOneTimeExpenseSchema,
+} from './one-time-expenses.schema';
+import * as oneTimeService from './one-time-expenses.service';
+
+const oneTimeExpensesRoutes: FastifyPluginAsync = async (fastify) => {
+ /**
+ * GET /users/:userId/one-time-expenses
+ * Lista gastos avulsos do usuário.
+ * Query params: ?month=6&year=2025
+ */
+ fastify.get<{ Params: { userId: string }; Querystring: Record<string, string> }>(
+ '/users/:userId/one-time-expenses',
+ async (request, reply) => {
+ const { userId } = oneTimeExpenseUserParamsSchema.parse(request.params);
+ const query = oneTimeExpenseQuerySchema.parse(request.query);
+ const result = await oneTimeService.findOneTimeExpensesByUser(userId, query);
+ return reply.send({ data: result });
+ },
+ );
+
+ /** GET /users/:userId/one-time-expenses/:id — Busca um gasto avulso específico */
+ fastify.get<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/one-time-expenses/:id',
+ async (request, reply) => {
+ const { userId, id } = oneTimeExpenseParamsSchema.parse(request.params);
+ const expense = await oneTimeService.findOneTimeExpenseById(userId, id);
+
+ if (!expense) {
+ return reply.status(404).send({ error: 'Gasto avulso não encontrado.' });
+ }
+ return reply.send({ data: expense });
+ },
+ );
+
+ /** POST /users/:userId/one-time-expenses — Adiciona um gasto avulso */
+ fastify.post<{ Params: { userId: string } }>(
+ '/users/:userId/one-time-expenses',
+ async (request, reply) => {
+ try {
+ const { userId } = oneTimeExpenseUserParamsSchema.parse(request.params);
+ const body = createOneTimeExpenseSchema.parse(request.body);
+ const expense = await oneTimeService.createOneTimeExpense(userId, body);
+ return reply.status(201).send({ data: expense });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ },
+ );
+
+ /** PUT /users/:userId/one-time-expenses/:id — Atualiza um gasto avulso */
+ fastify.put<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/one-time-expenses/:id',
+ async (request, reply) => {
+ try {
+ const { userId, id } = oneTimeExpenseParamsSchema.parse(request.params);
+ const body = updateOneTimeExpenseSchema.parse(request.body);
+ const expense = await oneTimeService.updateOneTimeExpense(userId, id, body);
+
+ if (!expense) {
+ return reply.status(404).send({ error: 'Gasto avulso não encontrado.' });
+ }
+ return reply.send({ data: expense });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ },
+ );
+
+ /** DELETE /users/:userId/one-time-expenses/:id — Remove um gasto avulso */
+ fastify.delete<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/one-time-expenses/:id',
+ async (request, reply) => {
+ const { userId, id } = oneTimeExpenseParamsSchema.parse(request.params);
+ const deleted = await oneTimeService.deleteOneTimeExpense(userId, id);
+
+ if (!deleted) {
+ return reply.status(404).send({ error: 'Gasto avulso não encontrado.' });
+ }
+ return reply.status(200).send({ data: deleted, message: 'Gasto avulso removido com sucesso.' });
+ },
+ );
+};
+
+export default oneTimeExpensesRoutes;
diff --git a/api/src/modules/one-time-expenses/one-time-expenses.schema.ts b/api/src/modules/one-time-expenses/one-time-expenses.schema.ts
new file mode 100644
index 0000000..9e32c8d
--- /dev/null
+++ b/api/src/modules/one-time-expenses/one-time-expenses.schema.ts
@@ -0,0 +1,54 @@
+import { z } from 'zod';
+
+const CATEGORIES = [
+ 'moradia',
+ 'alimentação',
+ 'transporte',
+ 'saúde',
+ 'educação',
+ 'lazer',
+ 'serviços',
+ 'outros',
+] as const;
+
+export const categoryEnum = z.enum(CATEGORIES);
+export const typeEnum = z.enum(['expense', 'income']);
+
+export const createOneTimeExpenseSchema = z.object({
+ name: z.string().min(1, 'Nome é obrigatório').max(100),
+ amount: z.number().positive('Valor deve ser positivo'),
+ category: categoryEnum.default('outros'),
+ type: typeEnum.default('expense'),
+ /** Data do gasto no formato YYYY-MM-DD. Default: hoje. */
+ expense_date: z
+ .string()
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato: YYYY-MM-DD')
+ .optional(),
+ notes: z.string().max(500).nullable().optional(),
+});
+
+export const updateOneTimeExpenseSchema = z.object({
+ name: z.string().min(1).max(100).optional(),
+ amount: z.number().positive().optional(),
+ category: categoryEnum.optional(),
+ type: typeEnum.optional(),
+ expense_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
+ notes: z.string().max(500).nullable().optional(),
+});
+
+export const oneTimeExpenseParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+ id: z.coerce.number().int().positive(),
+});
+
+export const oneTimeExpenseUserParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+});
+
+export const oneTimeExpenseQuerySchema = z.object({
+ month: z.coerce.number().int().min(1).max(12).optional(),
+ year: z.coerce.number().int().min(2020).optional(),
+});
+
+export type CreateOneTimeExpenseInput = z.infer<typeof createOneTimeExpenseSchema>;
+export type UpdateOneTimeExpenseInput = z.infer<typeof updateOneTimeExpenseSchema>;
diff --git a/api/src/modules/one-time-expenses/one-time-expenses.service.ts b/api/src/modules/one-time-expenses/one-time-expenses.service.ts
new file mode 100644
index 0000000..c55640d
--- /dev/null
+++ b/api/src/modules/one-time-expenses/one-time-expenses.service.ts
@@ -0,0 +1,102 @@
+import { and, between, eq } from 'drizzle-orm';
+import { db } from '@/db/client';
+import { oneTimeExpenses } from '@/db/schema';
+import type {
+ CreateOneTimeExpenseInput,
+ UpdateOneTimeExpenseInput,
+} from './one-time-expenses.schema';
+
+export async function findOneTimeExpensesByUser(
+ userId: number,
+ filters?: { month?: number; year?: number },
+) {
+ const conditions = [eq(oneTimeExpenses.user_id, userId)];
+
+ // Filtra pelo mês/ano usando range de datas no formato YYYY-MM-DD
+ if (filters?.month !== undefined && filters?.year !== undefined) {
+ const year = filters.year;
+ const month = filters.month;
+ const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
+ // Último dia do mês: pega o dia 0 do mês seguinte
+ const lastDay = new Date(year, month, 0).getDate();
+ const endDate = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
+ conditions.push(between(oneTimeExpenses.expense_date, startDate, endDate));
+ } else if (filters?.year !== undefined) {
+ const year = filters.year;
+ conditions.push(
+ between(oneTimeExpenses.expense_date, `${year}-01-01`, `${year}-12-31`),
+ );
+ }
+
+ return db
+ .select()
+ .from(oneTimeExpenses)
+ .where(and(...conditions))
+ .orderBy(oneTimeExpenses.expense_date);
+}
+
+export async function findOneTimeExpenseById(userId: number, id: number) {
+ const result = await db
+ .select()
+ .from(oneTimeExpenses)
+ .where(
+ and(eq(oneTimeExpenses.user_id, userId), eq(oneTimeExpenses.id, id)),
+ )
+ .limit(1);
+ return result[0] ?? null;
+}
+
+export async function createOneTimeExpense(
+ userId: number,
+ data: CreateOneTimeExpenseInput,
+) {
+ const [created] = await db
+ .insert(oneTimeExpenses)
+ .values({
+ user_id: userId,
+ name: data.name,
+ amount: data.amount,
+ category: data.category,
+ type: data.type,
+ expense_date: data.expense_date,
+ notes: data.notes ?? null,
+ })
+ .returning();
+ return created;
+}
+
+export async function updateOneTimeExpense(
+ userId: number,
+ id: number,
+ data: UpdateOneTimeExpenseInput,
+) {
+ const now = new Date().toISOString();
+
+ const [updated] = await db
+ .update(oneTimeExpenses)
+ .set({
+ ...(data.name !== undefined && { name: data.name }),
+ ...(data.amount !== undefined && { amount: data.amount }),
+ ...(data.category !== undefined && { category: data.category }),
+ ...(data.type !== undefined && { type: data.type }),
+ ...(data.expense_date !== undefined && { expense_date: data.expense_date }),
+ ...(data.notes !== undefined && { notes: data.notes }),
+ updated_at: now,
+ })
+ .where(
+ and(eq(oneTimeExpenses.user_id, userId), eq(oneTimeExpenses.id, id)),
+ )
+ .returning();
+
+ return updated ?? null;
+}
+
+export async function deleteOneTimeExpense(userId: number, id: number) {
+ const [deleted] = await db
+ .delete(oneTimeExpenses)
+ .where(
+ and(eq(oneTimeExpenses.user_id, userId), eq(oneTimeExpenses.id, id)),
+ )
+ .returning();
+ return deleted ?? null;
+}
diff --git a/api/src/modules/recurring-expenses/recurring-expenses.routes.ts b/api/src/modules/recurring-expenses/recurring-expenses.routes.ts
new file mode 100644
index 0000000..73594e8
--- /dev/null
+++ b/api/src/modules/recurring-expenses/recurring-expenses.routes.ts
@@ -0,0 +1,108 @@
+import type { FastifyPluginAsync } from 'fastify';
+import { ZodError } from 'zod';
+import {
+ createRecurringExpenseSchema,
+ recurringExpenseParamsSchema,
+ recurringExpenseUserParamsSchema,
+ updateRecurringExpenseSchema,
+} from './recurring-expenses.schema';
+import * as recurringService from './recurring-expenses.service';
+
+const recurringExpensesRoutes: FastifyPluginAsync = async (fastify) => {
+ /** GET /users/:userId/recurring-expenses — Lista todos os gastos recorrentes */
+ fastify.get<{ Params: { userId: string } }>(
+ '/users/:userId/recurring-expenses',
+ async (request, reply) => {
+ const { userId } = recurringExpenseUserParamsSchema.parse(request.params);
+ const result = await recurringService.findRecurringExpensesByUser(userId);
+ return reply.send({ data: result });
+ },
+ );
+
+ /** GET /users/:userId/recurring-expenses/:id — Busca um gasto recorrente específico */
+ fastify.get<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/recurring-expenses/:id',
+ async (request, reply) => {
+ const { userId, id } = recurringExpenseParamsSchema.parse(request.params);
+ const expense = await recurringService.findRecurringExpenseById(userId, id);
+
+ if (!expense) {
+ return reply.status(404).send({ error: 'Gasto recorrente não encontrado.' });
+ }
+ return reply.send({ data: expense });
+ },
+ );
+
+ /** POST /users/:userId/recurring-expenses — Cadastra um novo gasto fixo */
+ fastify.post<{ Params: { userId: string } }>(
+ '/users/:userId/recurring-expenses',
+ async (request, reply) => {
+ try {
+ const { userId } = recurringExpenseUserParamsSchema.parse(request.params);
+ const body = createRecurringExpenseSchema.parse(request.body);
+ const expense = await recurringService.createRecurringExpense(userId, body);
+ return reply.status(201).send({ data: expense });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ },
+ );
+
+ /** PUT /users/:userId/recurring-expenses/:id — Atualiza um gasto recorrente */
+ fastify.put<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/recurring-expenses/:id',
+ async (request, reply) => {
+ try {
+ const { userId, id } = recurringExpenseParamsSchema.parse(request.params);
+ const body = updateRecurringExpenseSchema.parse(request.body);
+ const expense = await recurringService.updateRecurringExpense(userId, id, body);
+
+ if (!expense) {
+ return reply.status(404).send({ error: 'Gasto recorrente não encontrado.' });
+ }
+ return reply.send({ data: expense });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ },
+ );
+
+ /**
+ * PATCH /users/:userId/recurring-expenses/:id/toggle
+ * Ativa ou desativa um gasto recorrente sem precisar deletar.
+ */
+ fastify.patch<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/recurring-expenses/:id/toggle',
+ async (request, reply) => {
+ const { userId, id } = recurringExpenseParamsSchema.parse(request.params);
+ const expense = await recurringService.toggleRecurringExpense(userId, id);
+
+ if (!expense) {
+ return reply.status(404).send({ error: 'Gasto recorrente não encontrado.' });
+ }
+ return reply.send({ data: expense });
+ },
+ );
+
+ /** DELETE /users/:userId/recurring-expenses/:id — Remove um gasto recorrente */
+ fastify.delete<{ Params: { userId: string; id: string } }>(
+ '/users/:userId/recurring-expenses/:id',
+ async (request, reply) => {
+ const { userId, id } = recurringExpenseParamsSchema.parse(request.params);
+ const deleted = await recurringService.deleteRecurringExpense(userId, id);
+
+ if (!deleted) {
+ return reply.status(404).send({ error: 'Gasto recorrente não encontrado.' });
+ }
+ return reply.status(200).send({ data: deleted, message: 'Gasto recorrente removido com sucesso.' });
+ },
+ );
+};
+
+export default recurringExpensesRoutes;
diff --git a/api/src/modules/recurring-expenses/recurring-expenses.schema.ts b/api/src/modules/recurring-expenses/recurring-expenses.schema.ts
new file mode 100644
index 0000000..8f9d473
--- /dev/null
+++ b/api/src/modules/recurring-expenses/recurring-expenses.schema.ts
@@ -0,0 +1,49 @@
+import { z } from 'zod';
+
+const CATEGORIES = [
+ 'moradia',
+ 'alimentação',
+ 'transporte',
+ 'saúde',
+ 'educação',
+ 'lazer',
+ 'serviços',
+ 'outros',
+] as const;
+
+export const categoryEnum = z.enum(CATEGORIES);
+export const typeEnum = z.enum(['expense', 'income']);
+
+export const createRecurringExpenseSchema = z.object({
+ name: z.string().min(1, 'Nome é obrigatório').max(100),
+ amount: z.number().positive('Valor deve ser positivo'),
+ category: categoryEnum.default('outros'),
+ type: typeEnum.default('expense'),
+ due_day: z.number().int().min(1).max(31).nullable().optional(),
+ is_active: z.boolean().default(true),
+ starts_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato: YYYY-MM-DD').optional(),
+ ends_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato: YYYY-MM-DD').nullable().optional(),
+});
+
+export const updateRecurringExpenseSchema = z.object({
+ name: z.string().min(1).max(100).optional(),
+ amount: z.number().positive().optional(),
+ category: categoryEnum.optional(),
+ type: typeEnum.optional(),
+ due_day: z.number().int().min(1).max(31).nullable().optional(),
+ is_active: z.boolean().optional(),
+ starts_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
+ ends_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
+});
+
+export const recurringExpenseParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+ id: z.coerce.number().int().positive(),
+});
+
+export const recurringExpenseUserParamsSchema = z.object({
+ userId: z.coerce.number().int().positive(),
+});
+
+export type CreateRecurringExpenseInput = z.infer<typeof createRecurringExpenseSchema>;
+export type UpdateRecurringExpenseInput = z.infer<typeof updateRecurringExpenseSchema>;
diff --git a/api/src/modules/recurring-expenses/recurring-expenses.service.ts b/api/src/modules/recurring-expenses/recurring-expenses.service.ts
new file mode 100644
index 0000000..2bb9720
--- /dev/null
+++ b/api/src/modules/recurring-expenses/recurring-expenses.service.ts
@@ -0,0 +1,115 @@
+import { and, eq } from 'drizzle-orm';
+import { db } from '@/db/client';
+import { recurringExpenses } from '@/db/schema';
+import type {
+ CreateRecurringExpenseInput,
+ UpdateRecurringExpenseInput,
+} from './recurring-expenses.schema';
+
+export async function findRecurringExpensesByUser(userId: number) {
+ return db
+ .select()
+ .from(recurringExpenses)
+ .where(eq(recurringExpenses.user_id, userId))
+ .orderBy(recurringExpenses.name);
+}
+
+export async function findRecurringExpenseById(userId: number, id: number) {
+ const result = await db
+ .select()
+ .from(recurringExpenses)
+ .where(
+ and(
+ eq(recurringExpenses.user_id, userId),
+ eq(recurringExpenses.id, id),
+ ),
+ )
+ .limit(1);
+ return result[0] ?? null;
+}
+
+export async function createRecurringExpense(
+ userId: number,
+ data: CreateRecurringExpenseInput,
+) {
+ const [created] = await db
+ .insert(recurringExpenses)
+ .values({
+ user_id: userId,
+ name: data.name,
+ amount: data.amount,
+ category: data.category,
+ type: data.type,
+ due_day: data.due_day ?? null,
+ is_active: data.is_active,
+ starts_at: data.starts_at,
+ ends_at: data.ends_at ?? null,
+ })
+ .returning();
+ return created;
+}
+
+export async function updateRecurringExpense(
+ userId: number,
+ id: number,
+ data: UpdateRecurringExpenseInput,
+) {
+ const now = new Date().toISOString();
+
+ const [updated] = await db
+ .update(recurringExpenses)
+ .set({
+ ...(data.name !== undefined && { name: data.name }),
+ ...(data.amount !== undefined && { amount: data.amount }),
+ ...(data.category !== undefined && { category: data.category }),
+ ...(data.type !== undefined && { type: data.type }),
+ ...(data.due_day !== undefined && { due_day: data.due_day }),
+ ...(data.is_active !== undefined && { is_active: data.is_active }),
+ ...(data.starts_at !== undefined && { starts_at: data.starts_at }),
+ ...(data.ends_at !== undefined && { ends_at: data.ends_at }),
+ updated_at: now,
+ })
+ .where(
+ and(
+ eq(recurringExpenses.user_id, userId),
+ eq(recurringExpenses.id, id),
+ ),
+ )
+ .returning();
+
+ return updated ?? null;
+}
+
+export async function toggleRecurringExpense(userId: number, id: number) {
+ const expense = await findRecurringExpenseById(userId, id);
+ if (!expense) return null;
+
+ const [updated] = await db
+ .update(recurringExpenses)
+ .set({
+ is_active: !expense.is_active,
+ updated_at: new Date().toISOString(),
+ })
+ .where(
+ and(
+ eq(recurringExpenses.user_id, userId),
+ eq(recurringExpenses.id, id),
+ ),
+ )
+ .returning();
+
+ return updated ?? null;
+}
+
+export async function deleteRecurringExpense(userId: number, id: number) {
+ const [deleted] = await db
+ .delete(recurringExpenses)
+ .where(
+ and(
+ eq(recurringExpenses.user_id, userId),
+ eq(recurringExpenses.id, id),
+ ),
+ )
+ .returning();
+ return deleted ?? null;
+}
diff --git a/api/src/modules/users/users.routes.ts b/api/src/modules/users/users.routes.ts
new file mode 100644
index 0000000..28cf584
--- /dev/null
+++ b/api/src/modules/users/users.routes.ts
@@ -0,0 +1,73 @@
+import type { FastifyPluginAsync } from 'fastify';
+import { ZodError } from 'zod';
+import {
+ createUserSchema,
+ updateUserSchema,
+ userParamsSchema,
+} from './users.schema';
+import * as usersService from './users.service';
+
+const usersRoutes: FastifyPluginAsync = async (fastify) => {
+ /** GET /users — Lista todos os usuários */
+ fastify.get('/users', async (_request, reply) => {
+ const result = await usersService.findAllUsers();
+ return reply.send({ data: result });
+ });
+
+ /** GET /users/:id — Busca um usuário pelo ID */
+ fastify.get<{ Params: { id: string } }>('/users/:id', async (request, reply) => {
+ const { id } = userParamsSchema.parse(request.params);
+ const user = await usersService.findUserById(id);
+
+ if (!user) {
+ return reply.status(404).send({ error: 'Usuário não encontrado.' });
+ }
+ return reply.send({ data: user });
+ });
+
+ /** POST /users — Cria um novo usuário */
+ fastify.post('/users', async (request, reply) => {
+ try {
+ const body = createUserSchema.parse(request.body);
+ const user = await usersService.createUser(body);
+ return reply.status(201).send({ data: user });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ });
+
+ /** PUT /users/:id — Atualiza um usuário (incluindo salário e override) */
+ fastify.put<{ Params: { id: string } }>('/users/:id', async (request, reply) => {
+ try {
+ const { id } = userParamsSchema.parse(request.params);
+ const body = updateUserSchema.parse(request.body);
+ const user = await usersService.updateUser(id, body);
+
+ if (!user) {
+ return reply.status(404).send({ error: 'Usuário não encontrado.' });
+ }
+ return reply.send({ data: user });
+ } catch (err) {
+ if (err instanceof ZodError) {
+ return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors });
+ }
+ throw err;
+ }
+ });
+
+ /** DELETE /users/:id — Remove um usuário (cascade nos gastos) */
+ fastify.delete<{ Params: { id: string } }>('/users/:id', async (request, reply) => {
+ const { id } = userParamsSchema.parse(request.params);
+ const deleted = await usersService.deleteUser(id);
+
+ if (!deleted) {
+ return reply.status(404).send({ error: 'Usuário não encontrado.' });
+ }
+ return reply.status(200).send({ data: deleted, message: 'Usuário removido com sucesso.' });
+ });
+};
+
+export default usersRoutes;
diff --git a/api/src/modules/users/users.schema.ts b/api/src/modules/users/users.schema.ts
new file mode 100644
index 0000000..62ab514
--- /dev/null
+++ b/api/src/modules/users/users.schema.ts
@@ -0,0 +1,23 @@
+import { z } from 'zod';
+
+export const createUserSchema = z.object({
+ name: z.string().min(1, 'Nome é obrigatório').max(100),
+ gross_salary: z.number().nonnegative('Salário bruto deve ser >= 0'),
+ net_salary_override: z.number().nonnegative().nullable().optional(),
+});
+
+export const updateUserSchema = z.object({
+ name: z.string().min(1).max(100).optional(),
+ gross_salary: z.number().nonnegative().optional(),
+ /**
+ * Envia `null` para limpar o override e voltar ao valor calculado automaticamente.
+ */
+ net_salary_override: z.number().nonnegative().nullable().optional(),
+});
+
+export const userParamsSchema = z.object({
+ id: z.coerce.number().int().positive(),
+});
+
+export type CreateUserInput = z.infer<typeof createUserSchema>;
+export type UpdateUserInput = z.infer<typeof updateUserSchema>;
diff --git a/api/src/modules/users/users.service.ts b/api/src/modules/users/users.service.ts
new file mode 100644
index 0000000..a0fda40
--- /dev/null
+++ b/api/src/modules/users/users.service.ts
@@ -0,0 +1,53 @@
+import { eq } from 'drizzle-orm';
+import { db } from '@/db/client';
+import { users } from '@/db/schema';
+import type { CreateUserInput, UpdateUserInput } from './users.schema';
+
+export async function findAllUsers() {
+ return db.select().from(users);
+}
+
+export async function findUserById(id: number) {
+ const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
+ return result[0] ?? null;
+}
+
+export async function createUser(data: CreateUserInput) {
+ const [created] = await db
+ .insert(users)
+ .values({
+ name: data.name,
+ gross_salary: data.gross_salary,
+ net_salary_override: data.net_salary_override ?? null,
+ })
+ .returning();
+ return created;
+}
+
+export async function updateUser(id: number, data: UpdateUserInput) {
+ const now = new Date().toISOString();
+
+ const [updated] = await db
+ .update(users)
+ .set({
+ ...(data.name !== undefined && { name: data.name }),
+ ...(data.gross_salary !== undefined && { gross_salary: data.gross_salary }),
+ // Permite setar null explicitamente (para remover o override)
+ ...(data.net_salary_override !== undefined && {
+ net_salary_override: data.net_salary_override,
+ }),
+ updated_at: now,
+ })
+ .where(eq(users.id, id))
+ .returning();
+
+ return updated ?? null;
+}
+
+export async function deleteUser(id: number) {
+ const [deleted] = await db
+ .delete(users)
+ .where(eq(users.id, id))
+ .returning();
+ return deleted ?? null;
+}
diff --git a/api/src/plugins/cors.ts b/api/src/plugins/cors.ts
new file mode 100644
index 0000000..5398e39
--- /dev/null
+++ b/api/src/plugins/cors.ts
@@ -0,0 +1,18 @@
+import cors from '@fastify/cors';
+import type { FastifyPluginAsync } from 'fastify';
+
+/**
+ * Plugin de CORS para o Fastify.
+ * Em produção, substitua o `origin: true` pelo domínio/IP do seu app mobile
+ * ou pelo endereço da sua VPS.
+ */
+const corsPlugin: FastifyPluginAsync = async (fastify) => {
+ fastify.register(cors, {
+ // Em dev: aceita qualquer origem
+ // Em prod: restrinja para o IP/domínio conhecido
+ origin: process.env.CORS_ORIGIN ?? true,
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
+ });
+};
+
+export default corsPlugin;
diff --git a/api/src/server.ts b/api/src/server.ts
new file mode 100644
index 0000000..6b3fd46
--- /dev/null
+++ b/api/src/server.ts
@@ -0,0 +1,89 @@
+import Fastify from 'fastify';
+import corsPlugin from './plugins/cors';
+import fastifyJwt from '@fastify/jwt';
+import authRoutes from './modules/auth/auth.routes';
+import dashboardRoutes from './modules/dashboard/dashboard.routes';
+import incomesRoutes from './modules/incomes/incomes.routes';
+import oneTimeExpensesRoutes from './modules/one-time-expenses/one-time-expenses.routes';
+import recurringExpensesRoutes from './modules/recurring-expenses/recurring-expenses.routes';
+import usersRoutes from './modules/users/users.routes';
+
+const PORT = Number(process.env.PORT) || 3333;
+const HOST = process.env.HOST ?? '0.0.0.0';
+
+const fastify = Fastify({
+ logger: {
+ transport:
+ process.env.NODE_ENV === 'development'
+ ? { target: 'pino-pretty', options: { colorize: true } }
+ : undefined,
+ },
+});
+
+// ---------------------------------------------------------------------------
+// Plugins globais
+// ---------------------------------------------------------------------------
+fastify.register(corsPlugin);
+
+fastify.register(fastifyJwt, {
+ secret: process.env.JWT_SECRET || 'super-secret-yaba-jwt-key-2026',
+});
+
+// ---------------------------------------------------------------------------
+// JWT Middleware
+// ---------------------------------------------------------------------------
+fastify.addHook('onRequest', async (request, reply) => {
+ if (request.url.includes('/api/v1/auth') || request.url.startsWith('/health')) {
+ return; // Pula autenticação para rotas públicas
+ }
+ try {
+ await request.jwtVerify();
+ } catch (err) {
+ return reply.status(401).send({ error: 'Não autorizado.' });
+ }
+});
+
+// ---------------------------------------------------------------------------
+// Rotas com prefixo /api/v1
+// ---------------------------------------------------------------------------
+const API_PREFIX = '/api/v1';
+
+fastify.register(authRoutes, { prefix: API_PREFIX });
+fastify.register(usersRoutes, { prefix: API_PREFIX });
+fastify.register(incomesRoutes, { prefix: API_PREFIX });
+fastify.register(recurringExpensesRoutes, { prefix: API_PREFIX });
+fastify.register(oneTimeExpensesRoutes, { prefix: API_PREFIX });
+fastify.register(dashboardRoutes, { prefix: API_PREFIX });
+
+// ---------------------------------------------------------------------------
+// Health check
+// ---------------------------------------------------------------------------
+fastify.get('/health', async () => ({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ version: '1.0.0',
+}));
+
+// ---------------------------------------------------------------------------
+// Handler de erros global
+// ---------------------------------------------------------------------------
+fastify.setErrorHandler((error, _request, reply) => {
+ fastify.log.error(error);
+ const statusCode = error.statusCode ?? 500;
+ reply.status(statusCode).send({
+ error: statusCode >= 500 ? 'Erro interno do servidor.' : error.message,
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Start
+// ---------------------------------------------------------------------------
+try {
+ await fastify.listen({ port: PORT, host: HOST });
+ console.log(`🚀 YABA API rodando em http://${HOST}:${PORT}`);
+ console.log(`📖 Health check: http://${HOST}:${PORT}/health`);
+} catch (err) {
+ fastify.log.error(err);
+ process.exit(1);
+}
diff --git a/api/src/utils/tax-calculator.ts b/api/src/utils/tax-calculator.ts
new file mode 100644
index 0000000..36ffc7e
--- /dev/null
+++ b/api/src/utils/tax-calculator.ts
@@ -0,0 +1,111 @@
+/**
+ * Calculadora de impostos brasileiros (INSS + IRRF).
+ * Tabelas vigentes: 2025.
+ *
+ * IMPORTANTE: As alíquotas e faixas mudam anualmente por decreto.
+ * Verifique a legislação vigente a cada ano fiscal.
+ *
+ * Fonte: Portaria MPS / IN RFB 2.141/2023 e atualizações.
+ */
+
+// ---------------------------------------------------------------------------
+// INSS — Tabela Progressiva 2025
+// ---------------------------------------------------------------------------
+const INSS_TABLE = [
+ { upTo: 1_518.0, rate: 0.075 },
+ { upTo: 2_793.88, rate: 0.09 },
+ { upTo: 4_190.83, rate: 0.12 },
+ { upTo: 8_157.41, rate: 0.14 },
+] as const;
+
+const INSS_TETO = 908.86; // Valor máximo de desconto do INSS
+
+/**
+ * Calcula o desconto do INSS com a tabela progressiva.
+ * Cada faixa incide apenas sobre a parte do salário dentro dela.
+ */
+export function calcularINSS(salarioBruto: number): number {
+ if (salarioBruto <= 0) return 0;
+ if (salarioBruto > INSS_TABLE[INSS_TABLE.length - 1].upTo) return INSS_TETO;
+
+ let inss = 0;
+ let salarioRestante = salarioBruto;
+ let limiteAnterior = 0;
+
+ for (const faixa of INSS_TABLE) {
+ if (salarioRestante <= 0) break;
+
+ const baseNaFaixa = Math.min(salarioBruto, faixa.upTo) - limiteAnterior;
+ inss += baseNaFaixa * faixa.rate;
+
+ salarioRestante -= baseNaFaixa;
+ limiteAnterior = faixa.upTo;
+ }
+
+ return parseFloat(inss.toFixed(2));
+}
+
+// ---------------------------------------------------------------------------
+// IRRF — Tabela Progressiva 2025
+// ---------------------------------------------------------------------------
+interface FaixaIRRF {
+ upTo: number | null; // null = sem limite superior
+ rate: number;
+ deduction: number;
+}
+
+const IRRF_TABLE: FaixaIRRF[] = [
+ { upTo: 2_259.2, rate: 0, deduction: 0 },
+ { upTo: 2_826.65, rate: 0.075, deduction: 169.44 },
+ { upTo: 3_751.05, rate: 0.15, deduction: 381.44 },
+ { upTo: 4_664.68, rate: 0.225, deduction: 662.77 },
+ { upTo: null, rate: 0.275, deduction: 896.0 },
+];
+
+/**
+ * Calcula o desconto do IRRF.
+ * Base de cálculo: Salário Bruto − INSS (sem considerar dependentes).
+ */
+export function calcularIRRF(salarioBruto: number, inss: number): number {
+ const baseCalculo = salarioBruto - inss;
+ if (baseCalculo <= 0) return 0;
+
+ const faixa = IRRF_TABLE.find(
+ (f) => f.upTo === null || baseCalculo <= f.upTo,
+ )!;
+
+ const irrf = baseCalculo * faixa.rate - faixa.deduction;
+ return parseFloat(Math.max(0, irrf).toFixed(2));
+}
+
+// ---------------------------------------------------------------------------
+// Resultado consolidado
+// ---------------------------------------------------------------------------
+export interface ResultadoCalculo {
+ salarioBruto: number;
+ descontoINSS: number;
+ descontoIRRF: number;
+ totalDescontos: number;
+ salarioLiquidoCalculado: number;
+}
+
+/**
+ * Calcula o salário líquido completo a partir do salário bruto.
+ * Retorna um objeto com o detalhamento de cada desconto.
+ */
+export function calcularSalarioLiquido(salarioBruto: number): ResultadoCalculo {
+ const descontoINSS = calcularINSS(salarioBruto);
+ const descontoIRRF = calcularIRRF(salarioBruto, descontoINSS);
+ const totalDescontos = parseFloat((descontoINSS + descontoIRRF).toFixed(2));
+ const salarioLiquidoCalculado = parseFloat(
+ (salarioBruto - totalDescontos).toFixed(2),
+ );
+
+ return {
+ salarioBruto,
+ descontoINSS,
+ descontoIRRF,
+ totalDescontos,
+ salarioLiquidoCalculado,
+ };
+}
diff --git a/api/tsconfig.json b/api/tsconfig.json
new file mode 100644
index 0000000..51e98fd
--- /dev/null
+++ b/api/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "types": ["bun-types"],
+ "strict": true,
+ "skipLibCheck": true,
+ "outDir": "./dist",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*", "drizzle.config.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/app.json b/app.json
index 334b661..3ca75ec 100644
--- a/app.json
+++ b/app.json
@@ -1,14 +1,19 @@
{
"expo": {
"name": "YABA",
- "slug": "YABA",
+ "slug": "yaba",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
- "scheme": "YABA",
+ "scheme": "yaba",
"userInterfaceStyle": "automatic",
"ios": {
- "icon": "./assets/expo.icon"
+ "icon": "./assets/expo.icon",
+ "bundleIdentifier": "com.lucas.yaba",
+ "infoPlist": {
+ "ITSAppUsesNonExemptEncryption": false,
+ "NSAllowsArbitraryLoads": true
+ }
},
"android": {
"adaptiveIcon": {
@@ -22,7 +27,7 @@
},
"web": {
"output": "static",
- "favicon": "./assets/images/favicon.png"
+ "favicon": "./assets/images/icon.png"
},
"plugins": [
"expo-router",
@@ -36,7 +41,15 @@
],
"expo-secure-store",
"expo-font",
- "expo-status-bar"
+ "expo-status-bar",
+ [
+ "expo-build-properties",
+ {
+ "android": {
+ "usesCleartextTraffic": true
+ }
+ }
+ ]
],
"experiments": {
"typedRoutes": true,
diff --git a/assets/.DS_Store b/assets/.DS_Store
index 59fbaf7..b2256f9 100644
--- a/assets/.DS_Store
+++ b/assets/.DS_Store
Binary files differ
diff --git a/bun.lock b/bun.lock
index 790417e..601434d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -11,6 +11,7 @@
"@react-navigation/bottom-tabs": "^7.18.16",
"expo": "~57.0.12",
"expo-blur": "~57.0.2",
+ "expo-build-properties": "~57.0.11",
"expo-constants": "~57.0.10",
"expo-dev-client": "~57.0.11",
"expo-device": "~57.0.1",
@@ -587,6 +588,8 @@
"expo-blur": ["expo-blur@57.0.2", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-Aoud8H8lmlNkbRufyvRLefmGFELdBf1n5Te/Xm+Zx8ORINH+aXL+gKb5mbftFSha860+I7pMArz77TBYz8HDVg=="],
+ "expo-build-properties": ["expo-build-properties@57.0.11", "", { "dependencies": { "@expo/schema-utils": "^57.0.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-+YlioKtW5rNVoGMGlw2t0pY198vjGYN/JcCQyc2sKhJYh3w2C4Lu72tkQAzNd/lLRIkUXTU7J0ICRQ8EaXTtbQ=="],
+
"expo-constants": ["expo-constants@57.0.10", "", { "dependencies": { "@expo/env": "~2.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-GCDXYEsloBfouMdT3BzoGhAkcLnYxEFNLQoSbNKIvIrD9FY5MmSeWuvRSveJdOiuydwQY2iH6hy020vTaQflCQ=="],
"expo-dev-client": ["expo-dev-client@57.0.11", "", { "dependencies": { "expo-dev-launcher": "~57.0.11", "expo-dev-menu": "~57.0.11", "expo-dev-menu-interface": "~57.0.0", "expo-manifests": "~57.0.1", "expo-updates-interface": "~57.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-IHatS97Vl0eQT8IG1Tsc10qDoyItxwjOw9AXLAbub+3uU7KljcqnXiCWRlhsI5KTjWV9/Dboc8psgiRb7oXXQw=="],
diff --git a/eas.json b/eas.json
index c6a91e8..4288d79 100644
--- a/eas.json
+++ b/eas.json
@@ -6,10 +6,16 @@
"build": {
"development": {
"developmentClient": true,
- "distribution": "internal"
+ "distribution": "internal",
+ "android": {
+ "buildType": "apk"
+ }
},
"preview": {
- "distribution": "internal"
+ "distribution": "internal",
+ "android": {
+ "buildType": "apk"
+ }
},
"production": {
"autoIncrement": true
diff --git a/package.json b/package.json
index 73ebdb4..58ea41b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"@react-navigation/bottom-tabs": "^7.18.16",
"expo": "~57.0.12",
"expo-blur": "~57.0.2",
+ "expo-build-properties": "~57.0.11",
"expo-constants": "~57.0.10",
"expo-dev-client": "~57.0.11",
"expo-device": "~57.0.1",
diff --git a/src/app/login.tsx b/src/app/login.tsx
index 15ec3be..16b6777 100644
--- a/src/app/login.tsx
+++ b/src/app/login.tsx
@@ -43,8 +43,8 @@ export default function LoginScreen() {
} else {
router.replace("/");
}
- } catch (error) {
- Alert.alert("Erro", "Falha ao fazer login. Verifique suas credenciais e tente novamente.");
+ } catch (error: any) {
+ Alert.alert("Erro", error.message || "Falha ao fazer login. Verifique suas credenciais e tente novamente.");
} finally {
setIsLoading(false);
}
diff --git a/src/app/register.tsx b/src/app/register.tsx
index aac588d..108761f 100644
--- a/src/app/register.tsx
+++ b/src/app/register.tsx
@@ -38,7 +38,7 @@ export default function RegisterScreen() {
router.replace('/');
}
} catch (err: any) {
- Alert.alert('Erro no cadastro', 'Verifique seus dados.');
+ Alert.alert('Erro no cadastro', err.message || 'Verifique seus dados.');
setIsLoading(false);
}
}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index d2fd7a9..2d0a725 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -75,6 +75,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
async function register(data: RegisterPayload) {
const res = await apiRegister(data);
+ console.log('Register response:', res);
await SecureStore.setItemAsync('yaba_token', res.token);
await SecureStore.setItemAsync('yaba_user', JSON.stringify(res.user));
setApiToken(res.token);
diff --git a/src/services/api.ts b/src/services/api.ts
index edd376e..d2a4f73 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -58,6 +58,7 @@ export async function apiRegister(data: any): Promise<{ user: User; token: strin
method: 'POST',
body: JSON.stringify(data),
});
+ console.log('API Register response:', result);
return result.data;
}