mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat: endurece mutações financeiras e permite zerar conta
This commit is contained in:
32
drizzle/0020_add-budget-invoice-unique-constraints.sql
Normal file
32
drizzle/0020_add-budget-invoice-unique-constraints.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "orcamentos"
|
||||||
|
WHERE "categoria_id" IS NOT NULL
|
||||||
|
AND "periodo" IS NOT NULL
|
||||||
|
GROUP BY "user_id", "categoria_id", "periodo"
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Nao foi possivel criar a unique de orcamentos: existem duplicatas por user_id, categoria_id e periodo.';
|
||||||
|
END IF;
|
||||||
|
END $$;--> statement-breakpoint
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "faturas"
|
||||||
|
WHERE "cartao_id" IS NOT NULL
|
||||||
|
AND "periodo" IS NOT NULL
|
||||||
|
GROUP BY "user_id", "cartao_id", "periodo"
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Nao foi possivel criar a unique de faturas: existem duplicatas por user_id, cartao_id e periodo.';
|
||||||
|
END IF;
|
||||||
|
END $$;--> statement-breakpoint
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "orcamentos_user_id_categoria_id_periodo_key" ON "orcamentos" USING btree ("user_id","categoria_id","periodo");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "faturas_user_id_cartao_id_periodo_key" ON "faturas" USING btree ("user_id","cartao_id","periodo");
|
||||||
@@ -93,12 +93,8 @@
|
|||||||
"name": "account_userId_user_id_fk",
|
"name": "account_userId_user_id_fk",
|
||||||
"tableFrom": "account",
|
"tableFrom": "account",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["userId"],
|
||||||
"userId"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -213,12 +209,8 @@
|
|||||||
"name": "tokens_api_user_id_user_id_fk",
|
"name": "tokens_api_user_id_user_id_fk",
|
||||||
"tableFrom": "tokens_api",
|
"tableFrom": "tokens_api",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -300,12 +292,8 @@
|
|||||||
"name": "orcamentos_user_id_user_id_fk",
|
"name": "orcamentos_user_id_user_id_fk",
|
||||||
"tableFrom": "orcamentos",
|
"tableFrom": "orcamentos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -313,12 +301,8 @@
|
|||||||
"name": "orcamentos_categoria_id_categorias_id_fk",
|
"name": "orcamentos_categoria_id_categorias_id_fk",
|
||||||
"tableFrom": "orcamentos",
|
"tableFrom": "orcamentos",
|
||||||
"tableTo": "categorias",
|
"tableTo": "categorias",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["categoria_id"],
|
||||||
"categoria_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -436,12 +420,8 @@
|
|||||||
"name": "cartoes_user_id_user_id_fk",
|
"name": "cartoes_user_id_user_id_fk",
|
||||||
"tableFrom": "cartoes",
|
"tableFrom": "cartoes",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -449,12 +429,8 @@
|
|||||||
"name": "cartoes_conta_id_contas_id_fk",
|
"name": "cartoes_conta_id_contas_id_fk",
|
||||||
"tableFrom": "cartoes",
|
"tableFrom": "cartoes",
|
||||||
"tableTo": "contas",
|
"tableTo": "contas",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["conta_id"],
|
||||||
"conta_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -536,12 +512,8 @@
|
|||||||
"name": "categorias_user_id_user_id_fk",
|
"name": "categorias_user_id_user_id_fk",
|
||||||
"tableFrom": "categorias",
|
"tableFrom": "categorias",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -656,12 +628,8 @@
|
|||||||
"name": "contas_user_id_user_id_fk",
|
"name": "contas_user_id_user_id_fk",
|
||||||
"tableFrom": "contas",
|
"tableFrom": "contas",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -820,12 +788,8 @@
|
|||||||
"name": "pre_lancamentos_user_id_user_id_fk",
|
"name": "pre_lancamentos_user_id_user_id_fk",
|
||||||
"tableFrom": "pre_lancamentos",
|
"tableFrom": "pre_lancamentos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -833,12 +797,8 @@
|
|||||||
"name": "pre_lancamentos_lancamento_id_lancamentos_id_fk",
|
"name": "pre_lancamentos_lancamento_id_lancamentos_id_fk",
|
||||||
"tableFrom": "pre_lancamentos",
|
"tableFrom": "pre_lancamentos",
|
||||||
"tableTo": "lancamentos",
|
"tableTo": "lancamentos",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lancamento_id"],
|
||||||
"lancamento_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "set null",
|
"onDelete": "set null",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -978,12 +938,8 @@
|
|||||||
"name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk",
|
"name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "lancamentos",
|
"tableTo": "lancamentos",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lancamento_id"],
|
||||||
"lancamento_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -991,12 +947,8 @@
|
|||||||
"name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk",
|
"name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "pagadores",
|
"tableTo": "pagadores",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["pagador_id"],
|
||||||
"pagador_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1004,12 +956,8 @@
|
|||||||
"name": "antecipacoes_parcelas_categoria_id_categorias_id_fk",
|
"name": "antecipacoes_parcelas_categoria_id_categorias_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "categorias",
|
"tableTo": "categorias",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["categoria_id"],
|
||||||
"categoria_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1017,12 +965,8 @@
|
|||||||
"name": "antecipacoes_parcelas_user_id_user_id_fk",
|
"name": "antecipacoes_parcelas_user_id_user_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1125,12 +1069,8 @@
|
|||||||
"name": "faturas_user_id_user_id_fk",
|
"name": "faturas_user_id_user_id_fk",
|
||||||
"tableFrom": "faturas",
|
"tableFrom": "faturas",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1138,12 +1078,8 @@
|
|||||||
"name": "faturas_cartao_id_cartoes_id_fk",
|
"name": "faturas_cartao_id_cartoes_id_fk",
|
||||||
"tableFrom": "faturas",
|
"tableFrom": "faturas",
|
||||||
"tableTo": "cartoes",
|
"tableTo": "cartoes",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["cartao_id"],
|
||||||
"cartao_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -1217,12 +1153,8 @@
|
|||||||
"name": "anotacoes_user_id_user_id_fk",
|
"name": "anotacoes_user_id_user_id_fk",
|
||||||
"tableFrom": "anotacoes",
|
"tableFrom": "anotacoes",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1310,12 +1242,8 @@
|
|||||||
"name": "passkey_userId_user_id_fk",
|
"name": "passkey_userId_user_id_fk",
|
||||||
"tableFrom": "passkey",
|
"tableFrom": "passkey",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["userId"],
|
||||||
"userId"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1398,12 +1326,8 @@
|
|||||||
"name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk",
|
"name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk",
|
||||||
"tableFrom": "compartilhamentos_pagador",
|
"tableFrom": "compartilhamentos_pagador",
|
||||||
"tableTo": "pagadores",
|
"tableTo": "pagadores",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["pagador_id"],
|
||||||
"pagador_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1411,12 +1335,8 @@
|
|||||||
"name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk",
|
"name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk",
|
||||||
"tableFrom": "compartilhamentos_pagador",
|
"tableFrom": "compartilhamentos_pagador",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["shared_with_user_id"],
|
||||||
"shared_with_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1424,12 +1344,8 @@
|
|||||||
"name": "compartilhamentos_pagador_created_by_user_id_user_id_fk",
|
"name": "compartilhamentos_pagador_created_by_user_id_user_id_fk",
|
||||||
"tableFrom": "compartilhamentos_pagador",
|
"tableFrom": "compartilhamentos_pagador",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["created_by_user_id"],
|
||||||
"created_by_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1585,12 +1501,8 @@
|
|||||||
"name": "pagadores_user_id_user_id_fk",
|
"name": "pagadores_user_id_user_id_fk",
|
||||||
"tableFrom": "pagadores",
|
"tableFrom": "pagadores",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1679,12 +1591,8 @@
|
|||||||
"name": "insights_salvos_user_id_user_id_fk",
|
"name": "insights_salvos_user_id_user_id_fk",
|
||||||
"tableFrom": "insights_salvos",
|
"tableFrom": "insights_salvos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1754,12 +1662,8 @@
|
|||||||
"name": "session_userId_user_id_fk",
|
"name": "session_userId_user_id_fk",
|
||||||
"tableFrom": "session",
|
"tableFrom": "session",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["userId"],
|
||||||
"userId"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1769,9 +1673,7 @@
|
|||||||
"session_token_unique": {
|
"session_token_unique": {
|
||||||
"name": "session_token_unique",
|
"name": "session_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["token"]
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
@@ -2140,12 +2042,8 @@
|
|||||||
"name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk",
|
"name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "antecipacoes_parcelas",
|
"tableTo": "antecipacoes_parcelas",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["antecipacao_id"],
|
||||||
"antecipacao_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "set null",
|
"onDelete": "set null",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -2153,12 +2051,8 @@
|
|||||||
"name": "lancamentos_user_id_user_id_fk",
|
"name": "lancamentos_user_id_user_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -2166,12 +2060,8 @@
|
|||||||
"name": "lancamentos_cartao_id_cartoes_id_fk",
|
"name": "lancamentos_cartao_id_cartoes_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "cartoes",
|
"tableTo": "cartoes",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["cartao_id"],
|
||||||
"cartao_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
},
|
},
|
||||||
@@ -2179,12 +2069,8 @@
|
|||||||
"name": "lancamentos_conta_id_contas_id_fk",
|
"name": "lancamentos_conta_id_contas_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "contas",
|
"tableTo": "contas",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["conta_id"],
|
||||||
"conta_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
},
|
},
|
||||||
@@ -2192,12 +2078,8 @@
|
|||||||
"name": "lancamentos_categoria_id_categorias_id_fk",
|
"name": "lancamentos_categoria_id_categorias_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "categorias",
|
"tableTo": "categorias",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["categoria_id"],
|
||||||
"categoria_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
},
|
},
|
||||||
@@ -2205,12 +2087,8 @@
|
|||||||
"name": "lancamentos_pagador_id_pagadores_id_fk",
|
"name": "lancamentos_pagador_id_pagadores_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "pagadores",
|
"tableTo": "pagadores",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["pagador_id"],
|
||||||
"pagador_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -2275,9 +2153,7 @@
|
|||||||
"user_email_unique": {
|
"user_email_unique": {
|
||||||
"name": "user_email_unique",
|
"name": "user_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["email"]
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
@@ -2355,12 +2231,8 @@
|
|||||||
"name": "preferencias_usuario_user_id_user_id_fk",
|
"name": "preferencias_usuario_user_id_user_id_fk",
|
||||||
"tableFrom": "preferencias_usuario",
|
"tableFrom": "preferencias_usuario",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -2370,9 +2242,7 @@
|
|||||||
"preferencias_usuario_user_id_unique": {
|
"preferencias_usuario_user_id_unique": {
|
||||||
"name": "preferencias_usuario_user_id_unique",
|
"name": "preferencias_usuario_user_id_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["user_id"]
|
||||||
"user_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|||||||
2367
drizzle/meta/0020_snapshot.json
Normal file
2367
drizzle/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -141,6 +141,13 @@
|
|||||||
"when": 1773699152928,
|
"when": 1773699152928,
|
||||||
"tag": "0019_ordinary_wild_pack",
|
"tag": "0019_ordinary_wild_pack",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773841892114,
|
||||||
|
"tag": "0020_add-budget-invoice-unique-constraints",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
const cardDialogAccounts = filterSources.accountRows.map(
|
const cardDialogAccounts = filterSources.accountRows.map(
|
||||||
(financialAccount: FinancialAccount) => ({
|
(financialAccount: FinancialAccount) => ({
|
||||||
id: financialAccount.id,
|
id: financialAccount.id,
|
||||||
name: financialAccount.name ?? "FinancialAccount",
|
name: financialAccount.name ?? "Conta",
|
||||||
logo: financialAccount.logo ?? null,
|
logo: financialAccount.logo ?? null,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -114,7 +114,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
filterSources.accountRows.find(
|
filterSources.accountRows.find(
|
||||||
(financialAccount: FinancialAccount) =>
|
(financialAccount: FinancialAccount) =>
|
||||||
financialAccount.id === card.accountId,
|
financialAccount.id === card.accountId,
|
||||||
)?.name ?? "FinancialAccount";
|
)?.name ?? "Conta";
|
||||||
|
|
||||||
const cardDialogData: Card = {
|
const cardDialogData: Card = {
|
||||||
id: card.id,
|
id: card.id,
|
||||||
|
|||||||
@@ -155,11 +155,11 @@ export default async function Page() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold mb-1 text-destructive">
|
<h2 className="text-lg font-bold mb-1 text-destructive">
|
||||||
Deletar conta
|
Ações perigosas
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
Ao prosseguir, sua conta e todos os dados associados serão
|
Você pode zerar os dados do OpenMonetis e manter seu acesso,
|
||||||
excluídos de forma irreversível.
|
ou excluir sua conta inteira de forma irreversível.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DeleteAccountForm />
|
<DeleteAccountForm />
|
||||||
|
|||||||
@@ -352,6 +352,9 @@ export const invoices = pgTable(
|
|||||||
table.cardId,
|
table.cardId,
|
||||||
table.period,
|
table.period,
|
||||||
),
|
),
|
||||||
|
userIdCardIdPeriodUnique: uniqueIndex(
|
||||||
|
"faturas_user_id_cartao_id_periodo_key",
|
||||||
|
).on(table.userId, table.cardId, table.period),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -380,6 +383,9 @@ export const budgets = pgTable(
|
|||||||
table.userId,
|
table.userId,
|
||||||
table.period,
|
table.period,
|
||||||
),
|
),
|
||||||
|
userIdCategoryIdPeriodUnique: uniqueIndex(
|
||||||
|
"orcamentos_user_id_categoria_id_periodo_key",
|
||||||
|
).on(table.userId, table.categoryId, table.period),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -635,11 +641,9 @@ export const transactions = pgTable(
|
|||||||
table.period,
|
table.period,
|
||||||
),
|
),
|
||||||
// Índice composto para o filtro quente do dashboard: userId + payerId + period
|
// Índice composto para o filtro quente do dashboard: userId + payerId + period
|
||||||
userIdPayerIdPeriodIdx: index("lancamentos_user_id_pagador_id_period_idx").on(
|
userIdPayerIdPeriodIdx: index(
|
||||||
table.userId,
|
"lancamentos_user_id_pagador_id_period_idx",
|
||||||
table.payerId,
|
).on(table.userId, table.payerId, table.period),
|
||||||
table.period,
|
|
||||||
),
|
|
||||||
// Índice para queries ordenadas por data de compra
|
// Índice para queries ordenadas por data de compra
|
||||||
userIdPurchaseDateIdx: index("lancamentos_user_id_purchase_date_idx").on(
|
userIdPurchaseDateIdx: index("lancamentos_user_id_purchase_date_idx").on(
|
||||||
table.userId,
|
table.userId,
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
|
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { categories, financialAccounts, transactions } from "@/db/schema";
|
||||||
categories,
|
|
||||||
financialAccounts,
|
|
||||||
payers,
|
|
||||||
transactions,
|
|
||||||
} from "@/db/schema";
|
|
||||||
import {
|
import {
|
||||||
INITIAL_BALANCE_CATEGORY_NAME,
|
INITIAL_BALANCE_CATEGORY_NAME,
|
||||||
INITIAL_BALANCE_CONDITION,
|
INITIAL_BALANCE_CONDITION,
|
||||||
@@ -22,7 +17,7 @@ import {
|
|||||||
} from "@/shared/lib/actions/helpers";
|
} from "@/shared/lib/actions/helpers";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
import { db } from "@/shared/lib/db";
|
import { db } from "@/shared/lib/db";
|
||||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||||
import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
|
import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
|
||||||
import {
|
import {
|
||||||
TRANSFER_CATEGORY_NAME,
|
TRANSFER_CATEGORY_NAME,
|
||||||
@@ -54,14 +49,20 @@ const accountBaseSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(1, "Selecione um logo."),
|
.min(1, "Selecione um logo."),
|
||||||
initialBalance: z
|
initialBalance: z
|
||||||
|
.union([
|
||||||
|
z.number(),
|
||||||
|
z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.transform((value) => (value.length === 0 ? "0" : value.replace(",", ".")))
|
.transform((value) =>
|
||||||
|
value.length === 0 ? "0" : value.replace(",", "."),
|
||||||
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(value) => !Number.isNaN(Number.parseFloat(value)),
|
(value) => !Number.isNaN(Number.parseFloat(value)),
|
||||||
"Informe um saldo inicial válido.",
|
"Informe um saldo inicial válido.",
|
||||||
)
|
)
|
||||||
.transform((value) => Number.parseFloat(value)),
|
.transform((value) => Number.parseFloat(value)),
|
||||||
|
]),
|
||||||
excludeFromBalance: z
|
excludeFromBalance: z
|
||||||
.union([z.boolean(), z.string()])
|
.union([z.boolean(), z.string()])
|
||||||
.transform((value) => value === true || value === "true"),
|
.transform((value) => value === true || value === "true"),
|
||||||
@@ -93,6 +94,15 @@ export async function createAccountAction(
|
|||||||
|
|
||||||
const normalizedInitialBalance = Math.abs(data.initialBalance);
|
const normalizedInitialBalance = Math.abs(data.initialBalance);
|
||||||
const hasInitialBalance = normalizedInitialBalance > 0;
|
const hasInitialBalance = normalizedInitialBalance > 0;
|
||||||
|
const adminPayerId = hasInitialBalance
|
||||||
|
? await getAdminPayerId(user.id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (hasInitialBalance && !adminPayerId) {
|
||||||
|
throw new Error(
|
||||||
|
"Payer com papel administrador não encontrado. Crie um pagador admin antes de definir um saldo inicial.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await db.transaction(async (tx: typeof db) => {
|
await db.transaction(async (tx: typeof db) => {
|
||||||
const [createdAccount] = await tx
|
const [createdAccount] = await tx
|
||||||
@@ -118,7 +128,7 @@ export async function createAccountAction(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [category, adminPagador] = await Promise.all([
|
const [category] = await Promise.all([
|
||||||
tx.query.categories.findFirst({
|
tx.query.categories.findFirst({
|
||||||
columns: { id: true },
|
columns: { id: true },
|
||||||
where: and(
|
where: and(
|
||||||
@@ -126,13 +136,6 @@ export async function createAccountAction(
|
|||||||
eq(categories.name, INITIAL_BALANCE_CATEGORY_NAME),
|
eq(categories.name, INITIAL_BALANCE_CATEGORY_NAME),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
tx.query.payers.findFirst({
|
|
||||||
columns: { id: true },
|
|
||||||
where: and(
|
|
||||||
eq(payers.userId, user.id),
|
|
||||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
@@ -141,12 +144,6 @@ export async function createAccountAction(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!adminPagador) {
|
|
||||||
throw new Error(
|
|
||||||
"Payer com papel administrador não encontrado. Crie um pagador admin antes de definir um saldo inicial.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { date, period } = getTodayInfo();
|
const { date, period } = getTodayInfo();
|
||||||
|
|
||||||
await tx.insert(transactions).values({
|
await tx.insert(transactions).values({
|
||||||
@@ -162,15 +159,15 @@ export async function createAccountAction(
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
accountId: createdAccount.id,
|
accountId: createdAccount.id,
|
||||||
categoryId: category.id,
|
categoryId: category.id,
|
||||||
payerId: adminPagador.id,
|
payerId: adminPayerId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("accounts");
|
revalidateForEntity("accounts", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "FinancialAccount criada com sucesso.",
|
message: "Conta criada com sucesso.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
@@ -209,15 +206,15 @@ export async function updateAccountAction(
|
|||||||
if (!updated) {
|
if (!updated) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "FinancialAccount não encontrada.",
|
error: "Conta não encontrada.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("accounts");
|
revalidateForEntity("accounts", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "FinancialAccount atualizada com sucesso.",
|
message: "Conta atualizada com sucesso.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
@@ -244,15 +241,15 @@ export async function deleteAccountAction(
|
|||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "FinancialAccount não encontrada.",
|
error: "Conta não encontrada.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("accounts");
|
revalidateForEntity("accounts", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "FinancialAccount removida com sucesso.",
|
message: "Conta removida com sucesso.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
@@ -261,8 +258,8 @@ export async function deleteAccountAction(
|
|||||||
|
|
||||||
// Transfer between accounts
|
// Transfer between accounts
|
||||||
const transferSchema = z.object({
|
const transferSchema = z.object({
|
||||||
fromAccountId: uuidSchema("FinancialAccount de origem"),
|
fromAccountId: uuidSchema("Conta de origem"),
|
||||||
toAccountId: uuidSchema("FinancialAccount de destino"),
|
toAccountId: uuidSchema("Conta de destino"),
|
||||||
amount: z
|
amount: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -299,6 +296,13 @@ export async function transferBetweenAccountsAction(
|
|||||||
|
|
||||||
// Generate a unique transfer ID to link both transactions
|
// Generate a unique transfer ID to link both transactions
|
||||||
const transferId = crypto.randomUUID();
|
const transferId = crypto.randomUUID();
|
||||||
|
const adminPayerId = await getAdminPayerId(user.id);
|
||||||
|
|
||||||
|
if (!adminPayerId) {
|
||||||
|
throw new Error(
|
||||||
|
"Payer administrador não encontrado. Por favor, crie um pagador admin.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await db.transaction(async (tx: typeof db) => {
|
await db.transaction(async (tx: typeof db) => {
|
||||||
// Verify both accounts exist and belong to the user
|
// Verify both accounts exist and belong to the user
|
||||||
@@ -320,21 +324,23 @@ export async function transferBetweenAccountsAction(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!fromAccount) {
|
if (!fromAccount) {
|
||||||
throw new Error("FinancialAccount de origem não encontrada.");
|
throw new Error("Conta de origem não encontrada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toAccount) {
|
if (!toAccount) {
|
||||||
throw new Error("FinancialAccount de destino não encontrada.");
|
throw new Error("Conta de destino não encontrada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the transfer category
|
// Get the transfer category and admin payer in parallel
|
||||||
const transferCategory = await tx.query.categories.findFirst({
|
const [transferCategory] = await Promise.all([
|
||||||
|
tx.query.categories.findFirst({
|
||||||
columns: { id: true },
|
columns: { id: true },
|
||||||
where: and(
|
where: and(
|
||||||
eq(categories.userId, user.id),
|
eq(categories.userId, user.id),
|
||||||
eq(categories.name, TRANSFER_CATEGORY_NAME),
|
eq(categories.name, TRANSFER_CATEGORY_NAME),
|
||||||
),
|
),
|
||||||
});
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!transferCategory) {
|
if (!transferCategory) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -342,62 +348,41 @@ export async function transferBetweenAccountsAction(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the admin payer
|
|
||||||
const adminPagador = await tx.query.payers.findFirst({
|
|
||||||
columns: { id: true },
|
|
||||||
where: and(
|
|
||||||
eq(payers.userId, user.id),
|
|
||||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!adminPagador) {
|
|
||||||
throw new Error(
|
|
||||||
"Payer administrador não encontrado. Por favor, crie um pagador admin.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const transferNote = `de ${fromAccount.name} -> ${toAccount.name}`;
|
const transferNote = `de ${fromAccount.name} -> ${toAccount.name}`;
|
||||||
|
|
||||||
// Create outgoing transaction (transfer from source account)
|
const sharedFields = {
|
||||||
await tx.insert(transactions).values({
|
|
||||||
condition: TRANSFER_CONDITION,
|
condition: TRANSFER_CONDITION,
|
||||||
|
paymentMethod: TRANSFER_PAYMENT_METHOD,
|
||||||
|
note: transferNote,
|
||||||
|
purchaseDate: data.date,
|
||||||
|
transactionType: "Transferência" as const,
|
||||||
|
period: data.period,
|
||||||
|
isSettled: true,
|
||||||
|
userId: user.id,
|
||||||
|
categoryId: transferCategory.id,
|
||||||
|
payerId: adminPayerId,
|
||||||
|
transferId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create both transactions in a single batch insert
|
||||||
|
await tx.insert(transactions).values([
|
||||||
|
{
|
||||||
|
...sharedFields,
|
||||||
name: TRANSFER_ESTABLISHMENT_SAIDA,
|
name: TRANSFER_ESTABLISHMENT_SAIDA,
|
||||||
paymentMethod: TRANSFER_PAYMENT_METHOD,
|
|
||||||
note: transferNote,
|
|
||||||
amount: formatDecimalForDbRequired(-Math.abs(data.amount)),
|
amount: formatDecimalForDbRequired(-Math.abs(data.amount)),
|
||||||
purchaseDate: data.date,
|
|
||||||
transactionType: "Transferência",
|
|
||||||
period: data.period,
|
|
||||||
isSettled: true,
|
|
||||||
userId: user.id,
|
|
||||||
accountId: fromAccount.id,
|
accountId: fromAccount.id,
|
||||||
categoryId: transferCategory.id,
|
},
|
||||||
payerId: adminPagador.id,
|
{
|
||||||
transferId,
|
...sharedFields,
|
||||||
});
|
|
||||||
|
|
||||||
// Create incoming transaction (transfer to destination account)
|
|
||||||
await tx.insert(transactions).values({
|
|
||||||
condition: TRANSFER_CONDITION,
|
|
||||||
name: TRANSFER_ESTABLISHMENT_ENTRADA,
|
name: TRANSFER_ESTABLISHMENT_ENTRADA,
|
||||||
paymentMethod: TRANSFER_PAYMENT_METHOD,
|
|
||||||
note: transferNote,
|
|
||||||
amount: formatDecimalForDbRequired(Math.abs(data.amount)),
|
amount: formatDecimalForDbRequired(Math.abs(data.amount)),
|
||||||
purchaseDate: data.date,
|
|
||||||
transactionType: "Transferência",
|
|
||||||
period: data.period,
|
|
||||||
isSettled: true,
|
|
||||||
userId: user.id,
|
|
||||||
accountId: toAccount.id,
|
accountId: toAccount.id,
|
||||||
categoryId: transferCategory.id,
|
},
|
||||||
payerId: adminPagador.id,
|
]);
|
||||||
transferId,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("accounts");
|
revalidateForEntity("accounts", user.id);
|
||||||
revalidateForEntity("transactions");
|
revalidateForEntity("transactions", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ import { AccountFormFields } from "./account-form-fields";
|
|||||||
import type { Account, AccountFormValues } from "./types";
|
import type { Account, AccountFormValues } from "./types";
|
||||||
|
|
||||||
const DEFAULT_ACCOUNT_TYPES = [
|
const DEFAULT_ACCOUNT_TYPES = [
|
||||||
"FinancialAccount Corrente",
|
"Conta Corrente",
|
||||||
"FinancialAccount Poupança",
|
"Conta Poupança",
|
||||||
"Carteira Digital",
|
"Carteira Digital",
|
||||||
"FinancialAccount Investimento",
|
"Conta Investimento",
|
||||||
"Pré-Pago | VR/VA",
|
"Pré-Pago | VR/VA",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ export function AccountDialog({
|
|||||||
const accountId = account?.id;
|
const accountId = account?.id;
|
||||||
|
|
||||||
if (mode === "update" && !accountId) {
|
if (mode === "update" && !accountId) {
|
||||||
const message = "FinancialAccount inválida.";
|
const message = "Conta inválida.";
|
||||||
setErrorMessage(message);
|
setErrorMessage(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ interface AccountFormFieldsProps {
|
|||||||
values: AccountFormValues;
|
values: AccountFormValues;
|
||||||
accountTypes: string[];
|
accountTypes: string[];
|
||||||
accountStatuses: string[];
|
accountStatuses: string[];
|
||||||
onChange: (field: keyof AccountFormValues, value: string) => void;
|
onChange: <K extends keyof AccountFormValues>(
|
||||||
|
field: K,
|
||||||
|
value: AccountFormValues[K],
|
||||||
|
) => void;
|
||||||
showInitialBalance?: boolean;
|
showInitialBalance?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +115,7 @@ export function AccountFormFields({
|
|||||||
id="exclude-from-balance"
|
id="exclude-from-balance"
|
||||||
checked={Boolean(values.excludeFromBalance)}
|
checked={Boolean(values.excludeFromBalance)}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
onChange("excludeFromBalance", checked ? "true" : "false")
|
onChange("excludeFromBalance", checked === true)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
@@ -129,10 +132,7 @@ export function AccountFormFields({
|
|||||||
id="exclude-initial-balance-from-income"
|
id="exclude-initial-balance-from-income"
|
||||||
checked={Boolean(values.excludeInitialBalanceFromIncome)}
|
checked={Boolean(values.excludeInitialBalanceFromIncome)}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
onChange(
|
onChange("excludeInitialBalanceFromIncome", checked === true)
|
||||||
"excludeInitialBalanceFromIncome",
|
|
||||||
checked ? "true" : "false",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export function TransferDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||||
<Label htmlFor="from-account">FinancialAccount de origem</Label>
|
<Label htmlFor="from-account">Conta de origem</Label>
|
||||||
<Select value={fromAccountId} disabled>
|
<Select value={fromAccountId} disabled>
|
||||||
<SelectTrigger id="from-account" className="w-full">
|
<SelectTrigger id="from-account" className="w-full">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
@@ -185,7 +185,7 @@ export function TransferDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||||
<Label htmlFor="to-account">FinancialAccount de destino</Label>
|
<Label htmlFor="to-account">Conta de destino</Label>
|
||||||
{availableAccounts.length === 0 ? (
|
{availableAccounts.length === 0 ? (
|
||||||
<div className="rounded-md border border-border bg-muted p-3 text-sm text-muted-foreground">
|
<div className="rounded-md border border-border bg-muted p-3 text-sm text-muted-foreground">
|
||||||
É necessário ter mais de uma conta cadastrada para realizar
|
É necessário ter mais de uma conta cadastrada para realizar
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export function SignupForm({ className, ...props }: DivProps) {
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setLoadingEmail(false);
|
setLoadingEmail(false);
|
||||||
toast.success("FinancialAccount criada com sucesso!");
|
toast.success("Conta criada com sucesso!");
|
||||||
router.replace("/dashboard");
|
router.replace("/dashboard");
|
||||||
},
|
},
|
||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { and, eq, ne } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { budgets, categories } from "@/db/schema";
|
import { budgets, categories } from "@/db/schema";
|
||||||
import {
|
import {
|
||||||
@@ -52,6 +52,28 @@ type BudgetCopyRow = {
|
|||||||
amount: unknown;
|
amount: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BUDGET_DUPLICATE_ERROR =
|
||||||
|
"Já existe um orçamento para esta categoria no período selecionado.";
|
||||||
|
const BUDGET_UNIQUE_CONSTRAINT = "orcamentos_user_id_categoria_id_periodo_key";
|
||||||
|
|
||||||
|
const hasUniqueConstraintError = (error: unknown, constraint: string) => {
|
||||||
|
if (!error || typeof error !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = error as {
|
||||||
|
code?: string;
|
||||||
|
constraint?: string;
|
||||||
|
cause?: { code?: string; constraint?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
(candidate.code === "23505" && candidate.constraint === constraint) ||
|
||||||
|
(candidate.cause?.code === "23505" &&
|
||||||
|
candidate.cause.constraint === constraint)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ensureCategory = async (userId: string, categoryId: string) => {
|
const ensureCategory = async (userId: string, categoryId: string) => {
|
||||||
const category = await db.query.categories.findFirst({
|
const category = await db.query.categories.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
@@ -79,36 +101,37 @@ export async function createBudgetAction(
|
|||||||
|
|
||||||
await ensureCategory(user.id, data.categoryId);
|
await ensureCategory(user.id, data.categoryId);
|
||||||
|
|
||||||
const duplicateConditions = [
|
const [createdBudget] = await db
|
||||||
eq(budgets.userId, user.id),
|
.insert(budgets)
|
||||||
eq(budgets.period, data.period),
|
.values({
|
||||||
eq(budgets.categoryId, data.categoryId),
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const duplicate = await db.query.budgets.findFirst({
|
|
||||||
columns: { id: true },
|
|
||||||
where: and(...duplicateConditions),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicate) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
"Já existe um orçamento para esta categoria no período selecionado.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.insert(budgets).values({
|
|
||||||
amount: formatDecimalForDbRequired(data.amount),
|
amount: formatDecimalForDbRequired(data.amount),
|
||||||
period: data.period,
|
period: data.period,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
categoryId: data.categoryId,
|
categoryId: data.categoryId,
|
||||||
});
|
})
|
||||||
|
.onConflictDoNothing({
|
||||||
|
target: [budgets.userId, budgets.categoryId, budgets.period],
|
||||||
|
})
|
||||||
|
.returning({ id: budgets.id });
|
||||||
|
|
||||||
revalidateForEntity("budgets");
|
if (!createdBudget) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: BUDGET_DUPLICATE_ERROR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidateForEntity("budgets", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Orçamento criado com sucesso." };
|
return { success: true, message: "Orçamento criado com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (hasUniqueConstraintError(error, BUDGET_UNIQUE_CONSTRAINT)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: BUDGET_DUPLICATE_ERROR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,26 +145,6 @@ export async function updateBudgetAction(
|
|||||||
|
|
||||||
await ensureCategory(user.id, data.categoryId);
|
await ensureCategory(user.id, data.categoryId);
|
||||||
|
|
||||||
const duplicateConditions = [
|
|
||||||
eq(budgets.userId, user.id),
|
|
||||||
eq(budgets.period, data.period),
|
|
||||||
eq(budgets.categoryId, data.categoryId),
|
|
||||||
ne(budgets.id, data.id),
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const duplicate = await db.query.budgets.findFirst({
|
|
||||||
columns: { id: true },
|
|
||||||
where: and(...duplicateConditions),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicate) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
"Já existe um orçamento para esta categoria no período selecionado.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(budgets)
|
.update(budgets)
|
||||||
.set({
|
.set({
|
||||||
@@ -159,10 +162,17 @@ export async function updateBudgetAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("budgets");
|
revalidateForEntity("budgets", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Orçamento atualizado com sucesso." };
|
return { success: true, message: "Orçamento atualizado com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (hasUniqueConstraintError(error, BUDGET_UNIQUE_CONSTRAINT)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: BUDGET_DUPLICATE_ERROR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +196,7 @@ export async function deleteBudgetAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("budgets");
|
revalidateForEntity("budgets", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Orçamento removido com sucesso." };
|
return { success: true, message: "Orçamento removido com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -247,21 +257,35 @@ export async function duplicatePreviousMonthBudgetsAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inserir novos orçamentos
|
// Inserir novos orçamentos sem falhar se houver corrida com outro request.
|
||||||
await db.insert(budgets).values(
|
const insertedBudgets = await db
|
||||||
|
.insert(budgets)
|
||||||
|
.values(
|
||||||
budgetsToCopy.map((b) => ({
|
budgetsToCopy.map((b) => ({
|
||||||
amount: b.amount as string,
|
amount: b.amount as string,
|
||||||
period: data.period,
|
period: data.period,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
categoryId: b.categoryId as string,
|
categoryId: b.categoryId as string,
|
||||||
})),
|
})),
|
||||||
);
|
)
|
||||||
|
.onConflictDoNothing({
|
||||||
|
target: [budgets.userId, budgets.categoryId, budgets.period],
|
||||||
|
})
|
||||||
|
.returning({ id: budgets.id });
|
||||||
|
|
||||||
revalidateForEntity("budgets");
|
if (insertedBudgets.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"Todas as categories do mês anterior já possuem orçamento neste mês.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidateForEntity("budgets", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `${budgetsToCopy.length} orçamento${budgetsToCopy.length > 1 ? "s" : ""} duplicado${budgetsToCopy.length > 1 ? "s" : ""} com sucesso.`,
|
message: `${insertedBudgets.length} orçamento${insertedBudgets.length > 1 ? "s" : ""} duplicado${insertedBudgets.length > 1 ? "s" : ""} com sucesso.`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async function assertAccountOwnership(userId: string, accountId: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error("FinancialAccount vinculada não encontrada.");
|
throw new Error("Conta vinculada não encontrada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export async function createCardAction(
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("cards");
|
revalidateForEntity("cards", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Cartão criado com sucesso." };
|
return { success: true, message: "Cartão criado com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -135,7 +135,7 @@ export async function updateCardAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("cards");
|
revalidateForEntity("cards", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Cartão atualizado com sucesso." };
|
return { success: true, message: "Cartão atualizado com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -162,7 +162,7 @@ export async function deleteCardAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("cards");
|
revalidateForEntity("cards", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Cartão removido com sucesso." };
|
return { success: true, message: "Cartão removido com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function createCategoryAction(
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("categories");
|
revalidateForEntity("categories", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Category criada com sucesso." };
|
return { success: true, message: "Category criada com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -114,7 +114,7 @@ export async function updateCategoryAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("categories");
|
revalidateForEntity("categories", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Category atualizada com sucesso." };
|
return { success: true, message: "Category atualizada com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -167,7 +167,7 @@ export async function deleteCategoryAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("categories");
|
revalidateForEntity("categories", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Category removida com sucesso." };
|
return { success: true, message: "Category removida com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { cards, categories, invoices, payers, transactions } from "@/db/schema";
|
import { cards, categories, invoices, transactions } from "@/db/schema";
|
||||||
import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants";
|
import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants";
|
||||||
import { revalidateForEntity } from "@/shared/lib/actions/helpers";
|
import { revalidateForEntity } from "@/shared/lib/actions/helpers";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type InvoicePaymentStatus,
|
type InvoicePaymentStatus,
|
||||||
PERIOD_FORMAT_REGEX,
|
PERIOD_FORMAT_REGEX,
|
||||||
} from "@/shared/lib/invoices";
|
} from "@/shared/lib/invoices";
|
||||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||||
import {
|
import {
|
||||||
getBusinessTodayDate,
|
getBusinessTodayDate,
|
||||||
parseLocalDateString,
|
parseLocalDateString,
|
||||||
@@ -60,6 +60,7 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
try {
|
try {
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = updateInvoicePaymentStatusSchema.parse(input);
|
const data = updateInvoicePaymentStatusSchema.parse(input);
|
||||||
|
const adminPayerId = await getAdminPayerId(user.id);
|
||||||
|
|
||||||
await db.transaction(async (tx: typeof db) => {
|
await db.transaction(async (tx: typeof db) => {
|
||||||
const card = await tx.query.cards.findFirst({
|
const card = await tx.query.cards.findFirst({
|
||||||
@@ -71,32 +72,20 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
throw new Error("Cartão não encontrado.");
|
throw new Error("Cartão não encontrado.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingInvoice = await tx.query.invoices.findFirst({
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
where: and(
|
|
||||||
eq(invoices.cardId, data.cardId),
|
|
||||||
eq(invoices.userId, user.id),
|
|
||||||
eq(invoices.period, data.period),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingInvoice) {
|
|
||||||
await tx
|
await tx
|
||||||
.update(invoices)
|
.insert(invoices)
|
||||||
.set({
|
.values({
|
||||||
paymentStatus: data.status,
|
|
||||||
})
|
|
||||||
.where(eq(invoices.id, existingInvoice.id));
|
|
||||||
} else {
|
|
||||||
await tx.insert(invoices).values({
|
|
||||||
cardId: data.cardId,
|
cardId: data.cardId,
|
||||||
period: data.period,
|
period: data.period,
|
||||||
paymentStatus: data.status,
|
paymentStatus: data.status,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [invoices.userId, invoices.cardId, invoices.period],
|
||||||
|
set: {
|
||||||
|
paymentStatus: data.status,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const shouldMarkAsPaid = data.status === INVOICE_PAYMENT_STATUS.PAID;
|
const shouldMarkAsPaid = data.status === INVOICE_PAYMENT_STATUS.PAID;
|
||||||
|
|
||||||
@@ -114,38 +103,26 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
|
const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
|
||||||
|
|
||||||
if (shouldMarkAsPaid) {
|
if (shouldMarkAsPaid) {
|
||||||
const [adminShareRow] = await tx
|
const [adminShareRow] = adminPayerId
|
||||||
|
? await tx
|
||||||
.select({
|
.select({
|
||||||
total: sql<number>`
|
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||||
coalesce(
|
|
||||||
sum(${transactions.amount}),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
})
|
})
|
||||||
.from(transactions)
|
.from(transactions)
|
||||||
.leftJoin(payers, eq(transactions.payerId, payers.id))
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(transactions.userId, user.id),
|
eq(transactions.userId, user.id),
|
||||||
eq(transactions.cardId, card.id),
|
eq(transactions.cardId, card.id),
|
||||||
eq(transactions.period, data.period),
|
eq(transactions.period, data.period),
|
||||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
eq(transactions.payerId, adminPayerId),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
: [{ total: 0 }];
|
||||||
|
|
||||||
const adminShare = Number(adminShareRow?.total ?? 0);
|
const adminShare = Number(adminShareRow?.total ?? 0);
|
||||||
const adminPayableAmount = Math.abs(Math.min(adminShare, 0));
|
const adminPayableAmount = Math.abs(Math.min(adminShare, 0));
|
||||||
|
|
||||||
if (adminPayableAmount > 0 && card.accountId) {
|
if (card.accountId && adminPayerId) {
|
||||||
const adminPagador = await tx.query.payers.findFirst({
|
|
||||||
columns: { id: true },
|
|
||||||
where: and(
|
|
||||||
eq(payers.userId, user.id),
|
|
||||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const paymentCategory = await tx.query.categories.findFirst({
|
const paymentCategory = await tx.query.categories.findFirst({
|
||||||
columns: { id: true },
|
columns: { id: true },
|
||||||
where: and(
|
where: and(
|
||||||
@@ -154,7 +131,6 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (adminPagador) {
|
|
||||||
// Usar a data customizada ou a data atual como data de pagamento
|
// Usar a data customizada ou a data atual como data de pagamento
|
||||||
const invoiceDate = data.paymentDate
|
const invoiceDate = data.paymentDate
|
||||||
? parseLocalDateString(data.paymentDate)
|
? parseLocalDateString(data.paymentDate)
|
||||||
@@ -174,7 +150,7 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
accountId: card.accountId,
|
accountId: card.accountId,
|
||||||
categoryId: paymentCategory?.id ?? null,
|
categoryId: paymentCategory?.id ?? null,
|
||||||
payerId: adminPagador.id,
|
payerId: adminPayerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingPayment = await tx.query.transactions.findFirst({
|
const existingPayment = await tx.query.transactions.findFirst({
|
||||||
@@ -194,7 +170,6 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
await tx.insert(transactions).values(payload);
|
await tx.insert(transactions).values(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await tx
|
await tx
|
||||||
.delete(transactions)
|
.delete(transactions)
|
||||||
@@ -207,7 +182,7 @@ export async function updateInvoicePaymentStatusAction(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("cards");
|
revalidateForEntity("cards", user.id);
|
||||||
|
|
||||||
return { success: true, message: successMessageByStatus[data.status] };
|
return { success: true, message: successMessageByStatus[data.status] };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -278,7 +253,7 @@ export async function updatePaymentDateAction(
|
|||||||
.where(eq(transactions.id, existingPayment.id));
|
.where(eq(transactions.id, existingPayment.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("cards");
|
revalidateForEntity("cards", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Data de pagamento atualizada." };
|
return { success: true, message: "Data de pagamento atualizada." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export async function createNoteAction(
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("notes");
|
revalidateForEntity("notes", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Anotação criada com sucesso." };
|
return { success: true, message: "Anotação criada com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -120,7 +120,7 @@ export async function updateNoteAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("notes");
|
revalidateForEntity("notes", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Anotação atualizada com sucesso." };
|
return { success: true, message: "Anotação atualizada com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -147,7 +147,7 @@ export async function deleteNoteAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("notes");
|
revalidateForEntity("notes", user.id);
|
||||||
|
|
||||||
return { success: true, message: "Anotação removida com sucesso." };
|
return { success: true, message: "Anotação removida com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -184,7 +184,7 @@ export async function archiveNoteAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidateForEntity("notes");
|
revalidateForEntity("notes", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ type ShareDeleteInput = z.infer<typeof shareDeleteSchema>;
|
|||||||
type ShareCodeJoinInput = z.infer<typeof shareCodeJoinSchema>;
|
type ShareCodeJoinInput = z.infer<typeof shareCodeJoinSchema>;
|
||||||
type ShareCodeRegenerateInput = z.infer<typeof shareCodeRegenerateSchema>;
|
type ShareCodeRegenerateInput = z.infer<typeof shareCodeRegenerateSchema>;
|
||||||
|
|
||||||
const revalidate = () => revalidateForEntity("payers");
|
const revalidate = (userId: string) => revalidateForEntity("payers", userId);
|
||||||
|
|
||||||
const generateShareCode = () => {
|
const generateShareCode = () => {
|
||||||
// base64url já retorna apenas [a-zA-Z0-9_-]
|
// base64url já retorna apenas [a-zA-Z0-9_-]
|
||||||
@@ -108,7 +108,7 @@ export async function createPayerAction(
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidate();
|
revalidate(user.id);
|
||||||
|
|
||||||
return { success: true, message: "Payer criado com sucesso." };
|
return { success: true, message: "Payer criado com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -158,7 +158,7 @@ export async function updatePayerAction(
|
|||||||
revalidatePath("/", "layout");
|
revalidatePath("/", "layout");
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidate();
|
revalidate(currentUser.id);
|
||||||
|
|
||||||
return { success: true, message: "Payer atualizado com sucesso." };
|
return { success: true, message: "Payer atualizado com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -195,7 +195,7 @@ export async function deletePayerAction(
|
|||||||
.delete(payers)
|
.delete(payers)
|
||||||
.where(and(eq(payers.id, data.id), eq(payers.userId, user.id)));
|
.where(and(eq(payers.id, data.id), eq(payers.userId, user.id)));
|
||||||
|
|
||||||
revalidate();
|
revalidate(user.id);
|
||||||
|
|
||||||
return { success: true, message: "Payer removido com sucesso." };
|
return { success: true, message: "Payer removido com sucesso." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -246,7 +246,7 @@ export async function joinPayerByShareCodeAction(
|
|||||||
createdByUserId: pagadorRow.userId,
|
createdByUserId: pagadorRow.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidate();
|
revalidate(user.id);
|
||||||
|
|
||||||
return { success: true, message: "Payer adicionado à sua lista." };
|
return { success: true, message: "Payer adicionado à sua lista." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -291,7 +291,7 @@ export async function deletePayerShareAction(
|
|||||||
|
|
||||||
await db.delete(payerShares).where(eq(payerShares.id, data.shareId));
|
await db.delete(payerShares).where(eq(payerShares.id, data.shareId));
|
||||||
|
|
||||||
revalidate();
|
revalidate(user.id);
|
||||||
revalidatePath(`/payers/${existing.payerId}`);
|
revalidatePath(`/payers/${existing.payerId}`);
|
||||||
|
|
||||||
return { success: true, message: "Compartilhamento removido." };
|
return { success: true, message: "Compartilhamento removido." };
|
||||||
@@ -325,7 +325,7 @@ export async function regeneratePayerShareCodeAction(
|
|||||||
.set({ shareCode: newCode })
|
.set({ shareCode: newCode })
|
||||||
.where(and(eq(payers.id, data.payerId), eq(payers.userId, user.id)));
|
.where(and(eq(payers.id, data.payerId), eq(payers.userId, user.id)));
|
||||||
|
|
||||||
revalidate();
|
revalidate(user.id);
|
||||||
revalidatePath(`/payers/${data.payerId}`);
|
revalidatePath(`/payers/${data.payerId}`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -2,14 +2,22 @@
|
|||||||
|
|
||||||
import { createHash, randomBytes } from "node:crypto";
|
import { createHash, randomBytes } from "node:crypto";
|
||||||
import { verifyPassword } from "better-auth/crypto";
|
import { verifyPassword } from "better-auth/crypto";
|
||||||
import { and, eq, isNull, ne } from "drizzle-orm";
|
import { and, eq, isNull, ne, or } from "drizzle-orm";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { account, apiTokens, payers } from "@/db/schema";
|
import { account, apiTokens, payers } from "@/db/schema";
|
||||||
|
import { revalidateForEntity } from "@/shared/lib/actions/helpers";
|
||||||
import { auth } from "@/shared/lib/auth/config";
|
import { auth } from "@/shared/lib/auth/config";
|
||||||
|
import { DEFAULT_CATEGORIES } from "@/shared/lib/categories/defaults";
|
||||||
import { db, schema } from "@/shared/lib/db";
|
import { db, schema } from "@/shared/lib/db";
|
||||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
import {
|
||||||
|
DEFAULT_PAYER_AVATAR,
|
||||||
|
PAYER_ROLE_ADMIN,
|
||||||
|
PAYER_STATUS_OPTIONS,
|
||||||
|
} from "@/shared/lib/payers/constants";
|
||||||
|
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||||
|
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
|
||||||
|
|
||||||
type ActionResponse<T = void> = {
|
type ActionResponse<T = void> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -50,11 +58,96 @@ const deleteAccountSchema = z.object({
|
|||||||
confirmation: z.literal("DELETAR"),
|
confirmation: z.literal("DELETAR"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resetAccountSchema = z.object({
|
||||||
|
confirmation: z.literal("ZERAR"),
|
||||||
|
});
|
||||||
|
|
||||||
const updatePreferencesSchema = z.object({
|
const updatePreferencesSchema = z.object({
|
||||||
statementNoteAsColumn: z.boolean(),
|
statementNoteAsColumn: z.boolean(),
|
||||||
transactionsColumnOrder: z.array(z.string()).nullable(),
|
transactionsColumnOrder: z.array(z.string()).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ResettableUser = {
|
||||||
|
name: string | null;
|
||||||
|
email: string | null;
|
||||||
|
image: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function resetUserAppData(
|
||||||
|
userId: string,
|
||||||
|
user: ResettableUser,
|
||||||
|
): Promise<void> {
|
||||||
|
const payerName =
|
||||||
|
(user.name && user.name.trim().length > 0
|
||||||
|
? user.name.trim()
|
||||||
|
: normalizeNameFromEmail(user.email)) || "Payer principal";
|
||||||
|
const avatarUrl = user.image ?? DEFAULT_PAYER_AVATAR;
|
||||||
|
const defaultPayerStatus = PAYER_STATUS_OPTIONS[0];
|
||||||
|
|
||||||
|
await db.transaction(async (tx: typeof db) => {
|
||||||
|
await tx
|
||||||
|
.delete(schema.payerShares)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
eq(schema.payerShares.sharedWithUserId, userId),
|
||||||
|
eq(schema.payerShares.createdByUserId, userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.delete(schema.userPreferences)
|
||||||
|
.where(eq(schema.userPreferences.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.apiTokens)
|
||||||
|
.where(eq(schema.apiTokens.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.savedInsights)
|
||||||
|
.where(eq(schema.savedInsights.userId, userId));
|
||||||
|
await tx.delete(schema.notes).where(eq(schema.notes.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.inboxItems)
|
||||||
|
.where(eq(schema.inboxItems.userId, userId));
|
||||||
|
await tx.delete(schema.budgets).where(eq(schema.budgets.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.installmentAnticipations)
|
||||||
|
.where(eq(schema.installmentAnticipations.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.transactions)
|
||||||
|
.where(eq(schema.transactions.userId, userId));
|
||||||
|
await tx.delete(schema.invoices).where(eq(schema.invoices.userId, userId));
|
||||||
|
await tx.delete(schema.cards).where(eq(schema.cards.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.financialAccounts)
|
||||||
|
.where(eq(schema.financialAccounts.userId, userId));
|
||||||
|
await tx.delete(schema.payers).where(eq(schema.payers.userId, userId));
|
||||||
|
await tx
|
||||||
|
.delete(schema.categories)
|
||||||
|
.where(eq(schema.categories.userId, userId));
|
||||||
|
|
||||||
|
if (DEFAULT_CATEGORIES.length > 0) {
|
||||||
|
await tx.insert(schema.categories).values(
|
||||||
|
DEFAULT_CATEGORIES.map((category) => ({
|
||||||
|
name: category.name,
|
||||||
|
type: category.type,
|
||||||
|
icon: category.icon,
|
||||||
|
userId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.insert(schema.payers).values({
|
||||||
|
name: payerName,
|
||||||
|
email: user.email,
|
||||||
|
avatarUrl,
|
||||||
|
status: defaultPayerStatus,
|
||||||
|
note: null,
|
||||||
|
role: PAYER_ROLE_ADMIN,
|
||||||
|
isAutoSend: false,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
export async function updateNameAction(
|
export async function updateNameAction(
|
||||||
@@ -74,6 +167,7 @@ export async function updateNameAction(
|
|||||||
|
|
||||||
const validated = updateNameSchema.parse(data);
|
const validated = updateNameSchema.parse(data);
|
||||||
const fullName = `${validated.firstName} ${validated.lastName}`;
|
const fullName = `${validated.firstName} ${validated.lastName}`;
|
||||||
|
const adminPayerId = await getAdminPayerId(session.user.id);
|
||||||
|
|
||||||
// Atualizar nome do usuário
|
// Atualizar nome do usuário
|
||||||
await db
|
await db
|
||||||
@@ -82,15 +176,14 @@ export async function updateNameAction(
|
|||||||
.where(eq(schema.user.id, session.user.id));
|
.where(eq(schema.user.id, session.user.id));
|
||||||
|
|
||||||
// Sincronizar nome com o pagador admin
|
// Sincronizar nome com o pagador admin
|
||||||
|
if (adminPayerId) {
|
||||||
await db
|
await db
|
||||||
.update(payers)
|
.update(payers)
|
||||||
.set({ name: fullName })
|
.set({ name: fullName })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(payers.userId, session.user.id), eq(payers.id, adminPayerId)),
|
||||||
eq(payers.userId, session.user.id),
|
|
||||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Revalidar o layout do dashboard para atualizar a sidebar
|
// Revalidar o layout do dashboard para atualizar a sidebar
|
||||||
revalidatePath("/", "layout");
|
revalidatePath("/", "layout");
|
||||||
@@ -251,7 +344,7 @@ export async function updateEmailAction(
|
|||||||
if (!storedHash) {
|
if (!storedHash) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "FinancialAccount de credencial não encontrada.",
|
error: "Conta de credencial não encontrada.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +441,7 @@ export async function deleteAccountAction(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "FinancialAccount deletada com sucesso",
|
message: "Conta deletada com sucesso.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
@@ -366,6 +459,75 @@ export async function deleteAccountAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetAccountAction(
|
||||||
|
data: z.infer<typeof resetAccountSchema>,
|
||||||
|
): Promise<ActionResponse> {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Não autenticado",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAccountSchema.parse(data);
|
||||||
|
|
||||||
|
const currentUser = await db.query.user.findFirst({
|
||||||
|
columns: {
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
where: eq(schema.user.id, session.user.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Usuário não encontrado.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetUserAppData(session.user.id, currentUser);
|
||||||
|
|
||||||
|
revalidateForEntity("accounts", session.user.id);
|
||||||
|
revalidateForEntity("cards", session.user.id);
|
||||||
|
revalidateForEntity("categories", session.user.id);
|
||||||
|
revalidateForEntity("budgets", session.user.id);
|
||||||
|
revalidateForEntity("payers", session.user.id);
|
||||||
|
revalidateForEntity("notes", session.user.id);
|
||||||
|
revalidateForEntity("transactions", session.user.id);
|
||||||
|
revalidateForEntity("inbox", session.user.id);
|
||||||
|
revalidatePath("/settings");
|
||||||
|
revalidatePath("/insights");
|
||||||
|
revalidatePath("/reports");
|
||||||
|
revalidatePath("/calendar");
|
||||||
|
revalidatePath("/", "layout");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Conta zerada com sucesso.",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.issues[0]?.message || "Dados inválidos",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Erro ao zerar conta:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erro ao zerar conta. Tente novamente.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function updatePreferencesAction(
|
export async function updatePreferencesAction(
|
||||||
data: z.infer<typeof updatePreferencesSchema>,
|
data: z.infer<typeof updatePreferencesSchema>,
|
||||||
): Promise<ActionResponse> {
|
): Promise<ActionResponse> {
|
||||||
@@ -557,7 +719,12 @@ export async function revokeApiTokenAction(
|
|||||||
.set({
|
.set({
|
||||||
revokedAt: new Date(),
|
revokedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(apiTokens.id, validated.tokenId));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiTokens.id, validated.tokenId),
|
||||||
|
eq(apiTokens.userId, session.user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
revalidatePath("/settings");
|
revalidatePath("/settings");
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { deleteAccountAction } from "@/features/settings/actions";
|
import {
|
||||||
|
deleteAccountAction,
|
||||||
|
resetAccountAction,
|
||||||
|
} from "@/features/settings/actions";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -16,59 +19,127 @@ import { Input } from "@/shared/components/ui/input";
|
|||||||
import { Label } from "@/shared/components/ui/label";
|
import { Label } from "@/shared/components/ui/label";
|
||||||
import { authClient } from "@/shared/lib/auth/client";
|
import { authClient } from "@/shared/lib/auth/client";
|
||||||
|
|
||||||
|
const RESET_CONFIRMATION = "ZERAR";
|
||||||
|
const DELETE_CONFIRMATION = "DELETAR";
|
||||||
|
|
||||||
|
type DangerAction = "reset" | "delete";
|
||||||
|
|
||||||
export function DeleteAccountForm() {
|
export function DeleteAccountForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [dangerAction, setDangerAction] = useState<DangerAction | null>(null);
|
||||||
const [confirmation, setConfirmation] = useState("");
|
const [confirmation, setConfirmation] = useState("");
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleAction = () => {
|
||||||
|
if (!dangerAction) return;
|
||||||
|
|
||||||
|
const currentAction = dangerAction;
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const result = await deleteAccountAction({
|
const result =
|
||||||
confirmation: confirmation as "DELETAR",
|
currentAction === "reset"
|
||||||
|
? await resetAccountAction({
|
||||||
|
confirmation: confirmation as typeof RESET_CONFIRMATION,
|
||||||
|
})
|
||||||
|
: await deleteAccountAction({
|
||||||
|
confirmation: confirmation as typeof DELETE_CONFIRMATION,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
// Fazer logout e redirecionar para página de login
|
|
||||||
|
if (currentAction === "delete") {
|
||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmation("");
|
||||||
|
setDangerAction(null);
|
||||||
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.error);
|
toast.error(result.error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
const handleOpenModal = (action: DangerAction) => {
|
||||||
setConfirmation("");
|
setConfirmation("");
|
||||||
setIsModalOpen(true);
|
setDangerAction(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
if (isPending) return;
|
if (isPending) return;
|
||||||
setConfirmation("");
|
setConfirmation("");
|
||||||
setIsModalOpen(false);
|
setDangerAction(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmationWord =
|
||||||
|
dangerAction === "reset" ? RESET_CONFIRMATION : DELETE_CONFIRMATION;
|
||||||
|
const isResetAction = dangerAction === "reset";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
<div className="space-y-4 max-w-md">
|
<div className="rounded-lg border p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Zerar conta</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Apaga todos os dados do OpenMonetis e deixa sua conta no estado
|
||||||
|
inicial, mantendo seu login e credenciais de acesso.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
||||||
|
<li>Lançamentos, faturas, antecipações e pré-lançamentos</li>
|
||||||
|
<li>Contas, cartões, orçamentos e anotações</li>
|
||||||
|
<li>Pagadores próprios e compartilhamentos recebidos</li>
|
||||||
|
<li>
|
||||||
|
Preferências do app, insights salvos e tokens do Companion
|
||||||
|
</li>
|
||||||
|
<li className="font-medium text-foreground">
|
||||||
|
Categorias padrão e pagador admin serão recriados
|
||||||
|
automaticamente
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleOpenModal("reset")}
|
||||||
|
disabled={isPending}
|
||||||
|
className="w-fit border-destructive/30 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
>
|
||||||
|
Zerar conta
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-destructive">Deletar conta</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Remove seu usuário e todos os dados associados de forma
|
||||||
|
permanente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className="list-disc list-inside text-sm text-destructive space-y-1">
|
<ul className="list-disc list-inside text-sm text-destructive space-y-1">
|
||||||
<li>Lançamentos, orçamentos e anotações</li>
|
<li>Lançamentos, orçamentos e anotações</li>
|
||||||
<li>Contas, cartões e categorias</li>
|
<li>Contas, cartões e categorias</li>
|
||||||
<li>Pagadores (incluindo o pagador padrão)</li>
|
<li>Pagadores, credenciais e configurações</li>
|
||||||
<li>Preferências e configurações</li>
|
|
||||||
<li className="font-bold">
|
<li className="font-bold">
|
||||||
Resumindo tudo, sua conta será permanentemente removida
|
Resumindo tudo, sua conta será permanentemente removida
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleOpenModal}
|
onClick={() => handleOpenModal("delete")}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
>
|
>
|
||||||
@@ -76,8 +147,17 @@ export function DeleteAccountForm() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
<Dialog
|
||||||
|
open={dangerAction !== null}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
handleCloseModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="max-w-md"
|
className="max-w-md"
|
||||||
onEscapeKeyDown={(e) => {
|
onEscapeKeyDown={(e) => {
|
||||||
@@ -88,24 +168,28 @@ export function DeleteAccountForm() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Você tem certeza?</DialogTitle>
|
<DialogTitle>
|
||||||
|
{isResetAction ? "Zerar sua conta?" : "Você tem certeza?"}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Essa ação não pode ser desfeita. Isso irá deletar permanentemente
|
{isResetAction
|
||||||
sua conta e remover seus dados de nossos servidores.
|
? "Essa ação não pode ser desfeita. Todos os dados do app serão apagados e sua conta voltará ao estado inicial, mas seu login continuará existindo."
|
||||||
|
: "Essa ação não pode ser desfeita. Isso irá deletar permanentemente sua conta e remover seus dados de nossos servidores."}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="confirmation">
|
<Label htmlFor="confirmation">
|
||||||
Para confirmar, digite <strong>DELETAR</strong> no campo abaixo.
|
Para confirmar, digite <strong>{confirmationWord}</strong> no
|
||||||
|
campo abaixo.
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="confirmation"
|
id="confirmation"
|
||||||
value={confirmation}
|
value={confirmation}
|
||||||
onChange={(e) => setConfirmation(e.target.value)}
|
onChange={(e) => setConfirmation(e.target.value)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
placeholder="DELETAR"
|
placeholder={confirmationWord}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,11 +206,22 @@ export function DeleteAccountForm() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant={isResetAction ? "outline" : "destructive"}
|
||||||
onClick={handleDelete}
|
onClick={handleAction}
|
||||||
disabled={isPending || confirmation !== "DELETAR"}
|
disabled={isPending || confirmation !== confirmationWord}
|
||||||
|
className={
|
||||||
|
isResetAction
|
||||||
|
? "border-destructive/30 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isPending ? "Deletando..." : "Deletar"}
|
{isPending
|
||||||
|
? isResetAction
|
||||||
|
? "Zerando..."
|
||||||
|
: "Deletando..."
|
||||||
|
: isResetAction
|
||||||
|
? "Zerar conta"
|
||||||
|
: "Deletar"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -263,10 +263,15 @@ export async function createInstallmentAnticipationAction(
|
|||||||
anticipationId: anticipation.id,
|
anticipationId: anticipation.id,
|
||||||
amount: "0", // Zera o valor para não contar em dobro
|
amount: "0", // Zera o valor para não contar em dobro
|
||||||
})
|
})
|
||||||
.where(inArray(transactions.id, data.installmentIds));
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(transactions.id, data.installmentIds),
|
||||||
|
eq(transactions.userId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("transactions");
|
revalidateForEntity("transactions", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -418,24 +423,37 @@ export async function cancelInstallmentAnticipationAction(
|
|||||||
amount: formatDecimalForDbRequired(originalValuePerInstallment),
|
amount: formatDecimalForDbRequired(originalValuePerInstallment),
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
|
and(
|
||||||
inArray(
|
inArray(
|
||||||
transactions.id,
|
transactions.id,
|
||||||
anticipation.anticipatedInstallmentIds as string[],
|
anticipation.anticipatedInstallmentIds as string[],
|
||||||
),
|
),
|
||||||
|
eq(transactions.userId, user.id),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Deletar lançamento de antecipação
|
// 5. Deletar lançamento de antecipação
|
||||||
await tx
|
await tx
|
||||||
.delete(transactions)
|
.delete(transactions)
|
||||||
.where(eq(transactions.id, anticipation.transactionId));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(transactions.id, anticipation.transactionId),
|
||||||
|
eq(transactions.userId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 6. Deletar registro de antecipação
|
// 6. Deletar registro de antecipação
|
||||||
await tx
|
await tx
|
||||||
.delete(installmentAnticipations)
|
.delete(installmentAnticipations)
|
||||||
.where(eq(installmentAnticipations.id, data.anticipationId));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(installmentAnticipations.id, data.anticipationId),
|
||||||
|
eq(installmentAnticipations.userId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidateForEntity("transactions");
|
revalidateForEntity("transactions", user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -49,16 +49,17 @@ const DASHBOARD_ENTITIES: ReadonlySet<string> = new Set([
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Revalidates paths for a specific entity.
|
* Revalidates paths for a specific entity.
|
||||||
* Also invalidates the dashboard "use cache" tag for financial entities.
|
* Also invalidates the user-scoped dashboard cache tag for financial entities.
|
||||||
* @param entity - The entity type
|
* @param entity - The entity type
|
||||||
*/
|
*/
|
||||||
export function revalidateForEntity(
|
export function revalidateForEntity(
|
||||||
entity: keyof typeof revalidateConfig,
|
entity: keyof typeof revalidateConfig,
|
||||||
|
userId: string,
|
||||||
): void {
|
): void {
|
||||||
revalidateConfig[entity].forEach((path) => revalidatePath(path));
|
revalidateConfig[entity].forEach((path) => revalidatePath(path));
|
||||||
|
|
||||||
// Invalidate dashboard cache for financial mutations
|
// Invalidate dashboard cache for financial mutations.
|
||||||
if (DASHBOARD_ENTITIES.has(entity)) {
|
if (DASHBOARD_ENTITIES.has(entity)) {
|
||||||
revalidateTag("dashboard", "max");
|
revalidateTag(`dashboard-${userId}`, "max");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const uuidSchema = (entityName: string = "ID") =>
|
|||||||
/**
|
/**
|
||||||
* Optional/nullable decimal string schema
|
* Optional/nullable decimal string schema
|
||||||
*/
|
*/
|
||||||
export const optionalDecimalSchema = z
|
export const optionalDecimalSchema = z.union([
|
||||||
|
z.number().nullable(),
|
||||||
|
z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -26,7 +28,8 @@ export const optionalDecimalSchema = z
|
|||||||
(value) => value === null || !Number.isNaN(Number.parseFloat(value)),
|
(value) => value === null || !Number.isNaN(Number.parseFloat(value)),
|
||||||
"Informe um valor numérico válido.",
|
"Informe um valor numérico válido.",
|
||||||
)
|
)
|
||||||
.transform((value) => (value === null ? null : Number.parseFloat(value)));
|
.transform((value) => (value === null ? null : Number.parseFloat(value))),
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Day of month schema (1-31)
|
* Day of month schema (1-31)
|
||||||
|
|||||||
Reference in New Issue
Block a user