@fayz-ai/plugin-agenda 0.13.4 → 0.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +686 -38
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -3243,17 +3243,6 @@ function CalendarView() {
|
|
|
3243
3243
|
}, []);
|
|
3244
3244
|
return /* @__PURE__ */ jsxs("div", { ref: shellRef, className: "flex", style: { height: shellHeight ?? void 0 }, children: [
|
|
3245
3245
|
/* @__PURE__ */ jsxs("div", { className: "hidden md:flex w-[220px] shrink-0 flex-col overflow-y-auto py-4 px-3 mr-4", children: [
|
|
3246
|
-
/* @__PURE__ */ jsxs(
|
|
3247
|
-
"button",
|
|
3248
|
-
{
|
|
3249
|
-
onClick: () => openModal("create"),
|
|
3250
|
-
className: "flex items-center gap-2 rounded-button bg-primary text-primary-foreground border border-primary pl-3 pr-4 py-1.5 text-sm font-semibold hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-all mb-6 w-fit",
|
|
3251
|
-
children: [
|
|
3252
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
3253
|
-
t2("agenda.sidebar.create")
|
|
3254
|
-
]
|
|
3255
|
-
}
|
|
3256
|
-
),
|
|
3257
3246
|
/* @__PURE__ */ jsx(MiniCalendar, { selectedDate, onDateSelect: handleMiniDateSelect }),
|
|
3258
3247
|
config.capabilities.professional && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3259
3248
|
/* @__PURE__ */ jsx("div", { className: "h-px bg-border my-4" }),
|
|
@@ -3282,7 +3271,7 @@ function CalendarView() {
|
|
|
3282
3271
|
) })
|
|
3283
3272
|
] })
|
|
3284
3273
|
] }),
|
|
3285
|
-
|
|
3274
|
+
/* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
|
|
3286
3275
|
PageActions,
|
|
3287
3276
|
{
|
|
3288
3277
|
actions: [
|
|
@@ -3329,29 +3318,15 @@ function CalendarView() {
|
|
|
3329
3318
|
] }),
|
|
3330
3319
|
!(isMobile && !mobileGridActive) && /* @__PURE__ */ jsx("h2", { className: "truncate text-base font-normal sm:text-lg", children: calendarTitle })
|
|
3331
3320
|
] }),
|
|
3332
|
-
/* @__PURE__ */
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
)) }),
|
|
3342
|
-
/* @__PURE__ */ jsx(
|
|
3343
|
-
Button,
|
|
3344
|
-
{
|
|
3345
|
-
variant: "outline",
|
|
3346
|
-
size: "icon",
|
|
3347
|
-
onClick: () => window.location.hash = "/settings/agenda",
|
|
3348
|
-
"aria-label": t2("agenda.settings.title"),
|
|
3349
|
-
title: t2("agenda.settings.title"),
|
|
3350
|
-
className: "ml-1 hidden md:inline-flex",
|
|
3351
|
-
children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
|
|
3352
|
-
}
|
|
3353
|
-
)
|
|
3354
|
-
] })
|
|
3321
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 shrink-0", children: /* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
3322
|
+
"button",
|
|
3323
|
+
{
|
|
3324
|
+
onClick: () => handleViewChange(opt.key),
|
|
3325
|
+
className: `rounded-md px-3 py-1 text-sm transition-colors ${currentView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
|
|
3326
|
+
children: t2(opt.labelKey)
|
|
3327
|
+
},
|
|
3328
|
+
opt.key
|
|
3329
|
+
)) }) })
|
|
3355
3330
|
] }),
|
|
3356
3331
|
/* @__PURE__ */ jsx("div", { className: "flex md:hidden items-center gap-1 overflow-x-auto px-3 pb-2", children: mobileViewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
3357
3332
|
"button",
|
|
@@ -4914,7 +4889,6 @@ var en = {
|
|
|
4914
4889
|
"agenda.sidebar.searchAgendas": "Find an agenda",
|
|
4915
4890
|
"agenda.sidebar.tooManyAgendas": "Up to {max} agendas at a time \u2014 uncheck one to add another.",
|
|
4916
4891
|
"agenda.sidebar.allLocations": "All locations",
|
|
4917
|
-
"agenda.sidebar.create": "Create",
|
|
4918
4892
|
"agenda.sidebar.location": "Location",
|
|
4919
4893
|
"agenda.sidebar.status": "Status",
|
|
4920
4894
|
"agenda.toolbar.next": "Next",
|
|
@@ -5138,7 +5112,6 @@ var ptBR = {
|
|
|
5138
5112
|
"agenda.sidebar.searchAgendas": "Buscar agenda",
|
|
5139
5113
|
"agenda.sidebar.tooManyAgendas": "At\xE9 {max} agendas por vez \u2014 desmarque uma para incluir outra.",
|
|
5140
5114
|
"agenda.sidebar.allLocations": "Todos os locais",
|
|
5141
|
-
"agenda.sidebar.create": "Criar",
|
|
5142
5115
|
"agenda.sidebar.location": "Local",
|
|
5143
5116
|
"agenda.sidebar.status": "Status",
|
|
5144
5117
|
"agenda.toolbar.next": "Pr\xF3ximo",
|
|
@@ -6927,6 +6900,678 @@ CREATE OR REPLACE VIEW public.v_appointments WITH (security_invoker = true) AS
|
|
|
6927
6900
|
LIMIT 1
|
|
6928
6901
|
) inv ON true;
|
|
6929
6902
|
`;
|
|
6903
|
+
var MIGRATION_007_O_CASCADE_LEVOU_A_AGENDA_JUNTO = `-- O CASCADE levou a agenda junto.
|
|
6904
|
+
--
|
|
6905
|
+
-- Sintoma: a tela de check-in (\`#/agenda/checkin\`) demora e n\xE3o abre. A causa
|
|
6906
|
+
-- n\xE3o est\xE1 na agenda \u2014 est\xE1 numa migration do FINANCEIRO, escrita tr\xEAs dias
|
|
6907
|
+
-- antes.
|
|
6908
|
+
--
|
|
6909
|
+
-- As migrations 044 e 045 do plugin-financial come\xE7am com:
|
|
6910
|
+
--
|
|
6911
|
+
-- DROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;
|
|
6912
|
+
--
|
|
6913
|
+
-- e recriam, logo abaixo, \`v_financial_invoice_balances\` e \`v_invoice_balances\`
|
|
6914
|
+
-- \u2014 as duas que o autor (eu) sabia que dependiam. O CASCADE, por\xE9m, n\xE3o se
|
|
6915
|
+
-- limita ao que o autor conhece: ele derruba a \xE1rvore inteira. Debaixo de
|
|
6916
|
+
-- \`v_invoice_balances\` moravam mais seis views, de dois plugins:
|
|
6917
|
+
--
|
|
6918
|
+
-- v_appointments (agenda \u2014 o read model do agendamento)
|
|
6919
|
+
-- \u251C\u2500\u2500 rep_appointments_by_period
|
|
6920
|
+
-- \u2502 \u2514\u2500\u2500 plg_agenda_rep_bookings
|
|
6921
|
+
-- \u251C\u2500\u2500 rep_confirmation_queue
|
|
6922
|
+
-- \u251C\u2500\u2500 rep_revenue_by_professional
|
|
6923
|
+
-- \u2514\u2500\u2500 rep_revenue_by_service
|
|
6924
|
+
-- plg_financial_rep_receivables (financeiro \u2014 restaurada na 053 de l\xE1)
|
|
6925
|
+
--
|
|
6926
|
+
-- \`v_appointments\` l\xEA \`v_invoice_balances\` desde a migration 006 desta corrente
|
|
6927
|
+
-- (o LATERAL que fez a agenda parar de apurar 8.134 faturas por agendamento).
|
|
6928
|
+
-- Essa depend\xEAncia foi o conserto de um problema real e continua certa; o que
|
|
6929
|
+
-- ela criou, sem ningu\xE9m notar, foi uma aresta do financeiro para a agenda \u2014
|
|
6930
|
+
-- e \xE9 por ela que o CASCADE passou.
|
|
6931
|
+
--
|
|
6932
|
+
-- Nada gritou. O \`DROP\` n\xE3o avisa o que levou, o \`apply\` das duas migrations
|
|
6933
|
+
-- passou verde, a lista de contas a pagar (o que eu estava consertando) ficou
|
|
6934
|
+
-- correta, e a agenda s\xF3 falha quando algu\xE9m a abre. O erro que chega ao
|
|
6935
|
+
-- navegador \xE9 um 404 do PostgREST sobre uma rela\xE7\xE3o que o c\xF3digo tem certeza
|
|
6936
|
+
-- de que existe \u2014 pista que aponta para o lugar errado.
|
|
6937
|
+
--
|
|
6938
|
+
-- \u2500\u2500 O que esta migration faz \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6939
|
+
--
|
|
6940
|
+
-- Recria as cinco views da agenda, com o corpo EXATO em que estavam: o de
|
|
6941
|
+
-- \`v_appointments\` vem da migration 006 (a forma com LATERAL, n\xE3o a do
|
|
6942
|
+
-- baseline), e os quatro relat\xF3rios v\xEAm do baseline, que n\xE3o os alterou desde
|
|
6943
|
+
-- ent\xE3o. Nenhuma coluna muda, nenhuma regra muda \u2014 o que muda \xE9 existirem.
|
|
6944
|
+
--
|
|
6945
|
+
-- \`WITH (security_invoker = true)\` expl\xEDcito em todas: uma view recriada n\xE3o
|
|
6946
|
+
-- herda op\xE7\xE3o nenhuma, e sem isso ela passa a rodar com os direitos do dono \u2014
|
|
6947
|
+
-- a RLS de quem l\xEA deixa de valer. Os GRANTs tamb\xE9m s\xE3o reescritos, porque
|
|
6948
|
+
-- morreram com as views.
|
|
6949
|
+
--
|
|
6950
|
+
-- \u2500\u2500 A li\xE7\xE3o, para a pr\xF3xima \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6951
|
+
--
|
|
6952
|
+
-- Antes de um \`DROP \u2026 CASCADE\`, pergunte ao banco quem depende:
|
|
6953
|
+
--
|
|
6954
|
+
-- SELECT DISTINCT dependente.relname
|
|
6955
|
+
-- FROM pg_depend d
|
|
6956
|
+
-- JOIN pg_rewrite r ON r.oid = d.objid
|
|
6957
|
+
-- JOIN pg_class dependente ON dependente.oid = r.ev_class
|
|
6958
|
+
-- WHERE d.refobjid = 'public.v_financial_invoice_balances'::regclass
|
|
6959
|
+
-- AND dependente.relname <> 'v_financial_invoice_balances';
|
|
6960
|
+
--
|
|
6961
|
+
-- A lista que voltar \xE9 a lista que a migration tem que recriar. E depois de
|
|
6962
|
+
-- aplicar, o diff entre as views que o c\xF3digo declara e as que o banco tem \xE9 o
|
|
6963
|
+
-- que pega o que escapou \u2014 foi ele que encontrou estas seis.
|
|
6964
|
+
--
|
|
6965
|
+
-- Idempotente.
|
|
6966
|
+
|
|
6967
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6968
|
+
-- 1. v_appointments \u2014 corpo da migration 006 (LATERAL sobre v_invoice_balances)
|
|
6969
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6970
|
+
|
|
6971
|
+
CREATE OR REPLACE VIEW public.v_appointments WITH (security_invoker = true) AS
|
|
6972
|
+
SELECT b.id,
|
|
6973
|
+
b.tenant_id,
|
|
6974
|
+
b.kind,
|
|
6975
|
+
b.starts_at,
|
|
6976
|
+
b.ends_at,
|
|
6977
|
+
b.status,
|
|
6978
|
+
b.notes,
|
|
6979
|
+
b.order_id,
|
|
6980
|
+
b.location_id,
|
|
6981
|
+
b.metadata,
|
|
6982
|
+
b.created_at,
|
|
6983
|
+
b.updated_at,
|
|
6984
|
+
b.party_id AS client_id,
|
|
6985
|
+
COALESCE(pc.name, b.metadata ->> 'title'::text) AS client_name,
|
|
6986
|
+
pc.phone AS client_phone,
|
|
6987
|
+
pc.email AS client_email,
|
|
6988
|
+
pc.avatar_url AS client_avatar_url,
|
|
6989
|
+
b.assignee_id AS professional_id,
|
|
6990
|
+
ps.name AS professional_name,
|
|
6991
|
+
ps.avatar_url AS professional_avatar_url,
|
|
6992
|
+
l.name AS location_name,
|
|
6993
|
+
o.total AS order_total,
|
|
6994
|
+
o.status AS order_status,
|
|
6995
|
+
EXTRACT(epoch FROM COALESCE(b.ends_at, b.starts_at) - b.starts_at)::integer / 60 AS total_duration_minutes,
|
|
6996
|
+
o.reference_number,
|
|
6997
|
+
o.status AS stage,
|
|
6998
|
+
o.metadata ->> 'direction'::text AS direction,
|
|
6999
|
+
( SELECT json_agg(json_build_object('id', oi.id, 'serviceId', oi.product_id, 'name', oi.name, 'durationMinutes', oi.duration_minutes, 'price', oi.unit_price, 'assigneeId', oi.assignee_id) ORDER BY oi.sort_order) AS json_agg
|
|
7000
|
+
FROM items oi
|
|
7001
|
+
WHERE oi.order_id = b.order_id) AS services,
|
|
7002
|
+
inv.status AS payment_status,
|
|
7003
|
+
inv.paid AS order_paid,
|
|
7004
|
+
inv.balance AS order_balance
|
|
7005
|
+
FROM appointments b
|
|
7006
|
+
LEFT JOIN people pc ON pc.id = b.party_id
|
|
7007
|
+
LEFT JOIN people ps ON ps.id = b.assignee_id
|
|
7008
|
+
LEFT JOIN app.units l ON l.id = b.location_id
|
|
7009
|
+
LEFT JOIN orders o ON o.id = b.order_id
|
|
7010
|
+
LEFT JOIN LATERAL (
|
|
7011
|
+
SELECT vb.status, vb.paid, vb.balance
|
|
7012
|
+
FROM v_invoice_balances vb
|
|
7013
|
+
WHERE vb.invoice_id = b.order_id
|
|
7014
|
+
LIMIT 1
|
|
7015
|
+
) inv ON true;
|
|
7016
|
+
|
|
7017
|
+
COMMENT ON VIEW public.v_appointments IS
|
|
7018
|
+
'O read model do agendamento: a linha do calend\xE1rio com cliente, '
|
|
7019
|
+
'profissional, unidade, itens e o estado de pagamento da Venda. L\xEA '
|
|
7020
|
+
'v_invoice_balances por LATERAL para que o filtro des\xE7a por baixo do '
|
|
7021
|
+
'agregado \u2014 como JOIN, a agenda apurava as faturas do tenant inteiro por '
|
|
7022
|
+
'agendamento.';
|
|
7023
|
+
|
|
7024
|
+
GRANT SELECT ON TABLE public.v_appointments TO authenticated, service_role;
|
|
7025
|
+
|
|
7026
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7027
|
+
-- 2. Os quatro relat\xF3rios que penduram nela, e o quinto que pendura no primeiro
|
|
7028
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7029
|
+
|
|
7030
|
+
CREATE OR REPLACE VIEW public.rep_appointments_by_period WITH (security_invoker = true) AS
|
|
7031
|
+
SELECT tenant_id,
|
|
7032
|
+
id AS booking_id,
|
|
7033
|
+
(starts_at)::date AS date,
|
|
7034
|
+
starts_at,
|
|
7035
|
+
client_id,
|
|
7036
|
+
client_name,
|
|
7037
|
+
professional_id,
|
|
7038
|
+
professional_name,
|
|
7039
|
+
COALESCE(((services -> 0) ->> 'name'::text), ((services -> 0) ->> 'serviceName'::text)) AS service_name,
|
|
7040
|
+
status,
|
|
7041
|
+
order_total AS revenue,
|
|
7042
|
+
created_at,
|
|
7043
|
+
updated_at
|
|
7044
|
+
FROM public.v_appointments b;
|
|
7045
|
+
|
|
7046
|
+
GRANT SELECT ON TABLE public.rep_appointments_by_period TO authenticated, service_role;
|
|
7047
|
+
|
|
7048
|
+
CREATE OR REPLACE VIEW public.plg_agenda_rep_bookings WITH (security_invoker = true) AS
|
|
7049
|
+
SELECT tenant_id,
|
|
7050
|
+
booking_id,
|
|
7051
|
+
date,
|
|
7052
|
+
starts_at,
|
|
7053
|
+
client_id,
|
|
7054
|
+
client_name,
|
|
7055
|
+
professional_id,
|
|
7056
|
+
professional_name,
|
|
7057
|
+
service_name,
|
|
7058
|
+
status,
|
|
7059
|
+
revenue AS booked_revenue,
|
|
7060
|
+
CASE
|
|
7061
|
+
WHEN (status = ANY (ARRAY['cancelled'::text, 'no_show'::text])) THEN (0)::numeric
|
|
7062
|
+
ELSE COALESCE(revenue, (0)::numeric)
|
|
7063
|
+
END AS effective_revenue,
|
|
7064
|
+
CASE
|
|
7065
|
+
WHEN (status = ANY (ARRAY['cancelled'::text, 'no_show'::text])) THEN COALESCE(revenue, (0)::numeric)
|
|
7066
|
+
ELSE (0)::numeric
|
|
7067
|
+
END AS lost_revenue,
|
|
7068
|
+
CASE
|
|
7069
|
+
WHEN (status = 'completed'::text) THEN 1
|
|
7070
|
+
ELSE 0
|
|
7071
|
+
END AS is_completed,
|
|
7072
|
+
CASE
|
|
7073
|
+
WHEN (status = 'cancelled'::text) THEN 1
|
|
7074
|
+
ELSE 0
|
|
7075
|
+
END AS is_cancelled,
|
|
7076
|
+
CASE
|
|
7077
|
+
WHEN (status = 'no_show'::text) THEN 1
|
|
7078
|
+
ELSE 0
|
|
7079
|
+
END AS is_no_show,
|
|
7080
|
+
created_at,
|
|
7081
|
+
updated_at
|
|
7082
|
+
FROM public.rep_appointments_by_period b;
|
|
7083
|
+
|
|
7084
|
+
GRANT SELECT ON TABLE public.plg_agenda_rep_bookings TO authenticated, service_role;
|
|
7085
|
+
|
|
7086
|
+
CREATE OR REPLACE VIEW public.rep_confirmation_queue WITH (security_invoker = true) AS
|
|
7087
|
+
SELECT tenant_id,
|
|
7088
|
+
id AS booking_id,
|
|
7089
|
+
(starts_at)::date AS date,
|
|
7090
|
+
starts_at,
|
|
7091
|
+
client_id,
|
|
7092
|
+
client_name,
|
|
7093
|
+
professional_id,
|
|
7094
|
+
professional_name,
|
|
7095
|
+
COALESCE(((services -> 0) ->> 'name'::text), ((services -> 0) ->> 'serviceName'::text)) AS service_name,
|
|
7096
|
+
'pending'::text AS confirmation_status,
|
|
7097
|
+
NULL::text AS confirmation_channel,
|
|
7098
|
+
NULL::timestamp with time zone AS confirmation_sent_at,
|
|
7099
|
+
NULL::timestamp with time zone AS confirmed_at,
|
|
7100
|
+
created_at,
|
|
7101
|
+
updated_at
|
|
7102
|
+
FROM public.v_appointments b
|
|
7103
|
+
WHERE ((status <> ALL (ARRAY['cancelled'::text, 'no_show'::text])) AND (starts_at >= (now() - '1 day'::interval)));
|
|
7104
|
+
|
|
7105
|
+
GRANT SELECT ON TABLE public.rep_confirmation_queue TO authenticated, service_role;
|
|
7106
|
+
|
|
7107
|
+
CREATE OR REPLACE VIEW public.rep_revenue_by_professional WITH (security_invoker = true) AS
|
|
7108
|
+
SELECT tenant_id,
|
|
7109
|
+
(starts_at)::date AS date,
|
|
7110
|
+
professional_id,
|
|
7111
|
+
COALESCE(professional_name, 'Sem profissional'::text) AS professional_name,
|
|
7112
|
+
(count(*))::integer AS appointment_count,
|
|
7113
|
+
(COALESCE(sum(NULLIF(order_total, (0)::numeric)), (0)::numeric))::numeric(14,2) AS total_revenue,
|
|
7114
|
+
(COALESCE(avg(NULLIF(order_total, (0)::numeric)), (0)::numeric))::numeric(14,2) AS avg_ticket,
|
|
7115
|
+
(0)::numeric(14,2) AS commission,
|
|
7116
|
+
max(updated_at) AS updated_at
|
|
7117
|
+
FROM public.v_appointments b
|
|
7118
|
+
WHERE (status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))
|
|
7119
|
+
GROUP BY tenant_id, ((starts_at)::date), professional_id, COALESCE(professional_name, 'Sem profissional'::text);
|
|
7120
|
+
|
|
7121
|
+
GRANT SELECT ON TABLE public.rep_revenue_by_professional TO authenticated, service_role;
|
|
7122
|
+
|
|
7123
|
+
CREATE OR REPLACE VIEW public.rep_revenue_by_service WITH (security_invoker = true) AS
|
|
7124
|
+
SELECT b.tenant_id,
|
|
7125
|
+
(b.starts_at)::date AS date,
|
|
7126
|
+
NULLIF((service_item.value ->> 'serviceId'::text), ''::text) AS service_id,
|
|
7127
|
+
COALESCE((service_item.value ->> 'name'::text), (service_item.value ->> 'serviceName'::text), 'Sem servico'::text) AS service_name,
|
|
7128
|
+
(count(DISTINCT b.id))::integer AS quantity,
|
|
7129
|
+
(COALESCE(sum((NULLIF((service_item.value ->> 'price'::text), ''::text))::numeric), (0)::numeric))::numeric(14,2) AS total_revenue,
|
|
7130
|
+
(COALESCE(avg((NULLIF((service_item.value ->> 'price'::text), ''::text))::numeric), (0)::numeric))::numeric(14,2) AS avg_ticket,
|
|
7131
|
+
max(b.updated_at) AS updated_at
|
|
7132
|
+
FROM (public.v_appointments b
|
|
7133
|
+
CROSS JOIN LATERAL jsonb_array_elements(COALESCE((b.services)::jsonb, '[]'::jsonb)) service_item(value))
|
|
7134
|
+
WHERE (b.status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))
|
|
7135
|
+
GROUP BY b.tenant_id, ((b.starts_at)::date), NULLIF((service_item.value ->> 'serviceId'::text), ''::text), COALESCE((service_item.value ->> 'name'::text), (service_item.value ->> 'serviceName'::text), 'Sem servico'::text);
|
|
7136
|
+
|
|
7137
|
+
GRANT SELECT ON TABLE public.rep_revenue_by_service TO authenticated, service_role;
|
|
7138
|
+
`;
|
|
7139
|
+
var MIGRATION_008_A_AGENDA_DO_V1_ATRAVESSA = `-- A agenda do V1 atravessa.
|
|
7140
|
+
--
|
|
7141
|
+
-- 354.206 agendamentos e 314.310 servi\xE7os agendados do Espa\xE7o Facial n\xE3o tinham
|
|
7142
|
+
-- para onde ir: \`appointments\` e \`appointment_services\` n\xE3o estavam nem na
|
|
7143
|
+
-- \`migration.allowlist\` nem na \`migration.excluded_tables\`. N\xE3o era uma decis\xE3o
|
|
7144
|
+
-- de deixar de fora \u2014 era a aus\xEAncia de decis\xE3o, e a tela de check-in abria
|
|
7145
|
+
-- vazia por isso.
|
|
7146
|
+
--
|
|
7147
|
+
-- \u2500\u2500 O de-para \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7148
|
+
--
|
|
7149
|
+
-- V1 appointments \u2192 public.appointments
|
|
7150
|
+
-- V1 appointment_services \u2192 public.appointment_items
|
|
7151
|
+
--
|
|
7152
|
+
-- Tr\xEAs refer\xEAncias o ledger resolve, porque as tr\xEAs j\xE1 atravessaram:
|
|
7153
|
+
-- \`client_id\` \u2192 \`public.people\` (123.304 linhas), \`professional_id\` \u2192
|
|
7154
|
+
-- \`public.people\` (300) e \`service_id\` \u2192 \`public.products\` (122).
|
|
7155
|
+
--
|
|
7156
|
+
-- \u2500\u2500 A unidade, que o V1 n\xE3o guarda no agendamento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7157
|
+
--
|
|
7158
|
+
-- \`public.appointments\` do V2 quer \`unit_id\` \u2014 \xE9 por ela que a rede de 63
|
|
7159
|
+
-- unidades filtra a agenda. O V1 n\xE3o tem essa coluna: ele guarda
|
|
7160
|
+
-- \`service_location_id\`, a SALA, e \xE9 a sala que sabe a unidade
|
|
7161
|
+
-- (\`service_locations.unit_id\`, um uuid que casa com \`companies.uuid\`).
|
|
7162
|
+
--
|
|
7163
|
+
-- A ponte \xE9 feita no extrator (\`augment\` do Step), n\xE3o aqui, porque ela \xE9
|
|
7164
|
+
-- conhecimento da ORIGEM. Medido em 19/09/2026, tenant 1:
|
|
7165
|
+
--
|
|
7166
|
+
-- agendamentos 354.206
|
|
7167
|
+
-- com sala que resolve a unidade 352.315 (99,5%)
|
|
7168
|
+
-- sem sala nenhuma 1.891
|
|
7169
|
+
--
|
|
7170
|
+
-- Os 1.891 sem sala entram com \`unit_id\` NULL \u2014 \xE9 o que o V1 tem, e inventar
|
|
7171
|
+
-- unidade para eles seria coloc\xE1-los na agenda de algu\xE9m.
|
|
7172
|
+
--
|
|
7173
|
+
-- \u2500\u2500 O estado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7174
|
+
--
|
|
7175
|
+
-- O vocabul\xE1rio do V1 tem sete palavras; o de \`appointment_kinds.appointment\`
|
|
7176
|
+
-- tem seis. Cinco s\xE3o a mesma palavra. Sobram duas, e o mapa \xE9 declarado no
|
|
7177
|
+
-- \`writer_args\` \u2014 dado, n\xE3o c\xF3digo:
|
|
7178
|
+
--
|
|
7179
|
+
-- unmarked \u2192 cancelled desmarcado \xE9 cancelado; o V2 n\xE3o tem a terceira
|
|
7180
|
+
-- palavra, e 'unmarked' n\xE3o \xE9 um estado, \xE9 a
|
|
7181
|
+
-- aus\xEAncia de um.
|
|
7182
|
+
-- waiting \u2192 confirmed a cliente chegou e espera. O V2 diz isso com
|
|
7183
|
+
-- \`checked_in_at\` (migration 003 desta corrente),
|
|
7184
|
+
-- que o pr\xF3prio V1 j\xE1 preenche \u2014 ent\xE3o a palavra
|
|
7185
|
+
-- vira 'confirmed' e o REL\xD3GIO carrega o resto.
|
|
7186
|
+
--
|
|
7187
|
+
-- O original fica em \`metadata.migration.source_status\`, como em toda tradu\xE7\xE3o
|
|
7188
|
+
-- de vocabul\xE1rio deste motor.
|
|
7189
|
+
--
|
|
7190
|
+
-- \u2500\u2500 O que N\xC3O vem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7191
|
+
--
|
|
7192
|
+
-- O agendamento do V1 n\xE3o tem Venda. \`order_id\` fica NULL, e \xE9 por isso que
|
|
7193
|
+
-- \`v_appointments\` traz \`order_total\`/\`payment_status\` vazios para o que
|
|
7194
|
+
-- migrou: o financeiro do V1 chega pela fatura (\`invoices\`), que \xE9 outra
|
|
7195
|
+
-- tabela e outro caminho. Costurar as duas \xE9 trabalho de reconcilia\xE7\xE3o, n\xE3o de
|
|
7196
|
+
-- transporte, e faz\xEA-lo aqui inventaria uma Venda que n\xE3o existe.
|
|
7197
|
+
--
|
|
7198
|
+
-- Idempotente.
|
|
7199
|
+
|
|
7200
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7201
|
+
-- 1. O vocabul\xE1rio fechado aprende duas palavras
|
|
7202
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7203
|
+
|
|
7204
|
+
-- A lista era LITERAL aqui, e literal \xE9 o que quebra a corrente: a restri\xE7\xE3o \xE9
|
|
7205
|
+
-- um vocabul\xE1rio fechado, e reescrev\xEA-la por extenso APAGA toda palavra que um
|
|
7206
|
+
-- passo anterior tenha acrescentado depois de algu\xE9m escrever esta lista. Foi o
|
|
7207
|
+
-- que aconteceu com \`fin_cash_session\`, registrado por \`financial/072\`, e com
|
|
7208
|
+
-- \`crm_pipeline\`, que o spine acrescenta antes das cadeias de plugin: a linha j\xE1
|
|
7209
|
+
-- existia no de-para e a restaurada n\xE3o a admitia \u2014
|
|
7210
|
+
-- \`check constraint "allowlist_writer_check" is violated by some row\`, e o
|
|
7211
|
+
-- encadeamento do zero parava aqui.
|
|
7212
|
+
--
|
|
7213
|
+
-- Vira acr\xE9scimo, que \xE9 como \`inventory/004\` j\xE1 fazia: l\xEA a defini\xE7\xE3o vigente,
|
|
7214
|
+
-- costura as duas palavras novas dentro dela e devolve. O que veio antes
|
|
7215
|
+
-- sobrevive, e o passo seguinte herda tudo.
|
|
7216
|
+
DO $$
|
|
7217
|
+
DECLARE v_def text;
|
|
7218
|
+
BEGIN
|
|
7219
|
+
SELECT pg_get_constraintdef(oid) INTO v_def
|
|
7220
|
+
FROM pg_constraint WHERE conname = 'allowlist_writer_check';
|
|
7221
|
+
IF v_def IS NULL OR strpos(v_def, 'appointment_item') > 0 THEN RETURN; END IF;
|
|
7222
|
+
ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
|
|
7223
|
+
EXECUTE replace(
|
|
7224
|
+
'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,
|
|
7225
|
+
'''person''::text',
|
|
7226
|
+
'''person''::text, ''appointment''::text, ''appointment_item''::text');
|
|
7227
|
+
END $$;
|
|
7228
|
+
|
|
7229
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7230
|
+
-- 2. O writer do agendamento
|
|
7231
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7232
|
+
|
|
7233
|
+
CREATE OR REPLACE FUNCTION migration.upsert_appointment(
|
|
7234
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
7235
|
+
) RETURNS jsonb
|
|
7236
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
7237
|
+
AS $function$
|
|
7238
|
+
DECLARE
|
|
7239
|
+
v_map jsonb := coalesce(p_args -> 'status_map', '{}'::jsonb);
|
|
7240
|
+
v_kind text := coalesce(p_args ->> 'kind', 'appointment');
|
|
7241
|
+
v_src_status text := p_row ->> 'status';
|
|
7242
|
+
v_status text;
|
|
7243
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
7244
|
+
v_starts timestamptz;
|
|
7245
|
+
v_ends timestamptz;
|
|
7246
|
+
v_dur integer;
|
|
7247
|
+
v_party uuid;
|
|
7248
|
+
v_assignee uuid;
|
|
7249
|
+
v_unit uuid;
|
|
7250
|
+
v_existing uuid;
|
|
7251
|
+
v_created boolean := false;
|
|
7252
|
+
v_standalone boolean := migration._standalone();
|
|
7253
|
+
BEGIN
|
|
7254
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
7255
|
+
IF p_id IS NULL THEN
|
|
7256
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an appointment needs an id');
|
|
7257
|
+
END IF;
|
|
7258
|
+
|
|
7259
|
+
-- A hora \xE9 o agendamento. Sem ela n\xE3o h\xE1 linha no calend\xE1rio, e uma linha
|
|
7260
|
+
-- sem hora n\xE3o \xE9 um agendamento incompleto: \xE9 outra coisa.
|
|
7261
|
+
v_starts := (p_row ->> 'starts_at')::timestamptz;
|
|
7262
|
+
IF v_starts IS NULL THEN
|
|
7263
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'an appointment needs starts_at');
|
|
7264
|
+
END IF;
|
|
7265
|
+
|
|
7266
|
+
IF NOT EXISTS (SELECT 1 FROM public.appointment_kinds k WHERE k.kind = v_kind) THEN
|
|
7267
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
7268
|
+
'reason', format('appointment kind %L is not declared in public.appointment_kinds', v_kind));
|
|
7269
|
+
END IF;
|
|
7270
|
+
|
|
7271
|
+
v_dur := nullif(p_row ->> 'duration_minutes', '')::integer;
|
|
7272
|
+
v_ends := v_starts + make_interval(mins => coalesce(v_dur, 0));
|
|
7273
|
+
|
|
7274
|
+
v_status := coalesce(v_map ->> coalesce(v_src_status, ''), v_map ->> '*', v_src_status, 'scheduled');
|
|
7275
|
+
-- \`appointment_kinds.statuses\` \xE9 text[], n\xE3o jsonb: o vocabul\xE1rio do tipo \xE9
|
|
7276
|
+
-- uma lista de palavras, e \`= ANY (array)\` \xE9 como se pergunta a ela.
|
|
7277
|
+
IF NOT EXISTS (SELECT 1 FROM public.appointment_kinds k
|
|
7278
|
+
WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN
|
|
7279
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
7280
|
+
'reason', format('status %L is not one of the statuses declared for kind %L', v_status, v_kind));
|
|
7281
|
+
END IF;
|
|
7282
|
+
v_meta := migration._deep_set(v_meta, ARRAY['migration', 'source_status'], to_jsonb(v_src_status));
|
|
7283
|
+
|
|
7284
|
+
BEGIN v_party := (p_row ->> 'party_id')::uuid; EXCEPTION WHEN OTHERS THEN v_party := NULL; END;
|
|
7285
|
+
BEGIN v_assignee := (p_row ->> 'assignee_id')::uuid; EXCEPTION WHEN OTHERS THEN v_assignee := NULL; END;
|
|
7286
|
+
BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;
|
|
7287
|
+
|
|
7288
|
+
SELECT tenant_id INTO v_existing FROM public.appointments WHERE id = p_id;
|
|
7289
|
+
IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
|
|
7290
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',
|
|
7291
|
+
'reason', format('appointment %s already exists in another tenant', p_id));
|
|
7292
|
+
END IF;
|
|
7293
|
+
|
|
7294
|
+
IF v_existing IS NULL THEN
|
|
7295
|
+
INSERT INTO public.appointments
|
|
7296
|
+
(id, tenant_id, kind, party_id, assignee_id, location_id, unit_id,
|
|
7297
|
+
starts_at, ends_at, status, notes, metadata, checked_in_at, created_at, updated_at)
|
|
7298
|
+
VALUES
|
|
7299
|
+
(p_id, p_tenant, v_kind, v_party, v_assignee, v_unit, v_unit,
|
|
7300
|
+
v_starts, v_ends, v_status, p_row ->> 'notes', v_meta,
|
|
7301
|
+
nullif(p_row ->> 'checked_in_at', '')::timestamptz,
|
|
7302
|
+
coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),
|
|
7303
|
+
coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));
|
|
7304
|
+
v_created := true;
|
|
7305
|
+
ELSE
|
|
7306
|
+
UPDATE public.appointments SET
|
|
7307
|
+
kind = v_kind,
|
|
7308
|
+
party_id = coalesce(v_party, party_id),
|
|
7309
|
+
assignee_id = coalesce(v_assignee, assignee_id),
|
|
7310
|
+
-- \`location_id\` \xE9 a unidade na leitura (v_appointments junta app.units
|
|
7311
|
+
-- por ela); \`unit_id\` \xE9 a unidade na RLS. As duas apontam para a mesma
|
|
7312
|
+
-- coisa e \xE9 isso que as mant\xE9m verdadeiras juntas.
|
|
7313
|
+
location_id = coalesce(v_unit, location_id),
|
|
7314
|
+
starts_at = v_starts,
|
|
7315
|
+
ends_at = v_ends,
|
|
7316
|
+
status = CASE WHEN p_row ? 'status' THEN v_status ELSE status END,
|
|
7317
|
+
notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,
|
|
7318
|
+
checked_in_at = CASE WHEN p_row ? 'checked_in_at'
|
|
7319
|
+
THEN nullif(p_row ->> 'checked_in_at', '')::timestamptz ELSE checked_in_at END,
|
|
7320
|
+
metadata = migration._merge_meta(metadata, v_meta),
|
|
7321
|
+
updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())
|
|
7322
|
+
WHERE id = p_id;
|
|
7323
|
+
END IF;
|
|
7324
|
+
|
|
7325
|
+
PERFORM migration._set_unit('public.appointments', p_id, v_unit);
|
|
7326
|
+
|
|
7327
|
+
IF v_standalone THEN
|
|
7328
|
+
PERFORM migration._audit(p_tenant, 'public.appointments', p_id,
|
|
7329
|
+
jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));
|
|
7330
|
+
END IF;
|
|
7331
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
7332
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
7333
|
+
END $function$;
|
|
7334
|
+
|
|
7335
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7336
|
+
-- 3. O writer do servi\xE7o agendado
|
|
7337
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7338
|
+
|
|
7339
|
+
CREATE OR REPLACE FUNCTION migration.upsert_appointment_item(
|
|
7340
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
7341
|
+
) RETURNS jsonb
|
|
7342
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
7343
|
+
AS $function$
|
|
7344
|
+
DECLARE
|
|
7345
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
7346
|
+
v_booking uuid;
|
|
7347
|
+
v_service uuid;
|
|
7348
|
+
v_name text;
|
|
7349
|
+
v_unit uuid;
|
|
7350
|
+
v_existing uuid;
|
|
7351
|
+
v_created boolean := false;
|
|
7352
|
+
v_standalone boolean := migration._standalone();
|
|
7353
|
+
BEGIN
|
|
7354
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
7355
|
+
IF p_id IS NULL THEN
|
|
7356
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an appointment item needs an id');
|
|
7357
|
+
END IF;
|
|
7358
|
+
|
|
7359
|
+
BEGIN v_booking := (p_row ->> 'booking_id')::uuid; EXCEPTION WHEN OTHERS THEN v_booking := NULL; END;
|
|
7360
|
+
-- Um item sem agendamento n\xE3o \xE9 um item solto, \xE9 um item perdido. Recusa em
|
|
7361
|
+
-- vez de escrever uma linha que nenhuma tela alcan\xE7a.
|
|
7362
|
+
IF v_booking IS NULL THEN
|
|
7363
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
7364
|
+
'reason', 'an appointment item needs the appointment it belongs to');
|
|
7365
|
+
END IF;
|
|
7366
|
+
|
|
7367
|
+
BEGIN v_service := (p_row ->> 'service_id')::uuid; EXCEPTION WHEN OTHERS THEN v_service := NULL; END;
|
|
7368
|
+
|
|
7369
|
+
-- O nome \xE9 NOT NULL no destino. A origem guarda o nome no SERVI\xC7O, n\xE3o no
|
|
7370
|
+
-- item, ent\xE3o ele vem pela ponte do extrator; se faltar, o nome do produto
|
|
7371
|
+
-- que a refer\xEAncia resolveu serve, e s\xF3 ent\xE3o o r\xF3tulo gen\xE9rico.
|
|
7372
|
+
v_name := coalesce(
|
|
7373
|
+
nullif(p_row ->> 'name', ''),
|
|
7374
|
+
(SELECT pr.name FROM public.products pr WHERE pr.id = v_service),
|
|
7375
|
+
'Servi\xE7o');
|
|
7376
|
+
|
|
7377
|
+
SELECT a.unit_id INTO v_unit FROM public.appointments a WHERE a.id = v_booking;
|
|
7378
|
+
|
|
7379
|
+
SELECT tenant_id INTO v_existing FROM public.appointment_items WHERE id = p_id;
|
|
7380
|
+
IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
|
|
7381
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',
|
|
7382
|
+
'reason', format('appointment item %s already exists in another tenant', p_id));
|
|
7383
|
+
END IF;
|
|
7384
|
+
|
|
7385
|
+
IF v_existing IS NULL THEN
|
|
7386
|
+
INSERT INTO public.appointment_items
|
|
7387
|
+
(id, tenant_id, booking_id, service_id, name, duration_minutes, price,
|
|
7388
|
+
sort_order, unit_id, metadata, created_at, updated_at)
|
|
7389
|
+
VALUES
|
|
7390
|
+
(p_id, p_tenant, v_booking, v_service, v_name,
|
|
7391
|
+
nullif(p_row ->> 'duration_minutes', '')::integer,
|
|
7392
|
+
coalesce(nullif(p_row ->> 'price', '')::numeric, 0),
|
|
7393
|
+
coalesce(nullif(p_row ->> 'sort_order', '')::integer, 0),
|
|
7394
|
+
v_unit, v_meta,
|
|
7395
|
+
coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),
|
|
7396
|
+
coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));
|
|
7397
|
+
v_created := true;
|
|
7398
|
+
ELSE
|
|
7399
|
+
UPDATE public.appointment_items SET
|
|
7400
|
+
booking_id = v_booking,
|
|
7401
|
+
service_id = coalesce(v_service, service_id),
|
|
7402
|
+
name = v_name,
|
|
7403
|
+
duration_minutes = CASE WHEN p_row ? 'duration_minutes'
|
|
7404
|
+
THEN nullif(p_row ->> 'duration_minutes', '')::integer ELSE duration_minutes END,
|
|
7405
|
+
unit_id = coalesce(v_unit, unit_id),
|
|
7406
|
+
metadata = migration._merge_meta(metadata, v_meta),
|
|
7407
|
+
updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())
|
|
7408
|
+
WHERE id = p_id;
|
|
7409
|
+
END IF;
|
|
7410
|
+
|
|
7411
|
+
IF v_standalone THEN
|
|
7412
|
+
PERFORM migration._audit(p_tenant, 'public.appointment_items', p_id,
|
|
7413
|
+
jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));
|
|
7414
|
+
END IF;
|
|
7415
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
7416
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
7417
|
+
END $function$;
|
|
7418
|
+
|
|
7419
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7420
|
+
-- 4. As duas linhas do de-para
|
|
7421
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7422
|
+
--
|
|
7423
|
+
-- A allowlist congela a linha depois de escrita \u2014 mudar o de-para \xE9 um arquivo
|
|
7424
|
+
-- de migration, n\xE3o um UPDATE avulso, e o gatilho \`trg_allowlist_frozen\` \xE9 quem
|
|
7425
|
+
-- cobra isso. Este \xC9 o arquivo de migration, ent\xE3o ele destrava; \`set_local\`
|
|
7426
|
+
-- para a destrava morrer com a transa\xE7\xE3o, tenha ela terminado bem ou mal.
|
|
7427
|
+
|
|
7428
|
+
SET LOCAL migration.allowlist_unlock = 'on';
|
|
7429
|
+
|
|
7430
|
+
SELECT migration._allow(
|
|
7431
|
+
'appointments', 'public.appointments', 'agenda', 'appointment',
|
|
7432
|
+
jsonb_build_object(
|
|
7433
|
+
'kind', 'appointment',
|
|
7434
|
+
'status_map', jsonb_build_object(
|
|
7435
|
+
'scheduled', 'scheduled',
|
|
7436
|
+
'confirmed', 'confirmed',
|
|
7437
|
+
'in_progress', 'in_progress',
|
|
7438
|
+
'completed', 'completed',
|
|
7439
|
+
'no_show', 'no_show',
|
|
7440
|
+
'unmarked', 'cancelled',
|
|
7441
|
+
'waiting', 'confirmed')),
|
|
7442
|
+
NULL::text[],
|
|
7443
|
+
jsonb_build_object(
|
|
7444
|
+
'uuid', 'id',
|
|
7445
|
+
'appointment_date', 'starts_at',
|
|
7446
|
+
'duration_minutes', 'duration_minutes',
|
|
7447
|
+
'status', 'status',
|
|
7448
|
+
'notes', 'notes',
|
|
7449
|
+
'checked_in_at', 'checked_in_at',
|
|
7450
|
+
'created_at', 'created_at',
|
|
7451
|
+
'updated_at', 'updated_at',
|
|
7452
|
+
'client_id', 'party_id@clients',
|
|
7453
|
+
'professional_id', 'assignee_id@professionals',
|
|
7454
|
+
'unit_company_id', 'unit_id@companies',
|
|
7455
|
+
'tenant_id', '-',
|
|
7456
|
+
'checkout_confirmed_by', '-',
|
|
7457
|
+
'price', 'jsonb:agenda.v1_price',
|
|
7458
|
+
'is_prepaid', 'jsonb:agenda.is_prepaid',
|
|
7459
|
+
'prepaid_invoice_item_id', 'jsonb:agenda.v1_prepaid_invoice_item_ref',
|
|
7460
|
+
'checkout_confirmed_at', 'jsonb:agenda.checkout_confirmed_at',
|
|
7461
|
+
'service_location_id', 'jsonb:agenda.v1_service_location_ref',
|
|
7462
|
+
'opportunity_id', 'jsonb:crm.v1_opportunity_ref',
|
|
7463
|
+
'whatsapp_sent', 'jsonb:agenda.whatsapp_sent',
|
|
7464
|
+
'whatsapp_confirmed', 'jsonb:agenda.whatsapp_confirmed'),
|
|
7465
|
+
'O Compromisso do V1. A unidade n\xE3o vem de uma coluna: vem da SALA '
|
|
7466
|
+
'(service_location_id \u2192 service_locations.unit_id \u2192 companies.uuid), e a '
|
|
7467
|
+
'ponte \xE9 feita no extrator. 99,5% resolvem; os 1.891 sem sala entram sem '
|
|
7468
|
+
'unidade, que \xE9 o que a origem tem. Sem Venda: order_id fica NULL porque o '
|
|
7469
|
+
'financeiro do V1 chega pela fatura, que \xE9 outro caminho.'
|
|
7470
|
+
);
|
|
7471
|
+
|
|
7472
|
+
SELECT migration._allow(
|
|
7473
|
+
'appointment_services', 'public.appointment_items', 'agenda', 'appointment_item',
|
|
7474
|
+
'{}'::jsonb,
|
|
7475
|
+
NULL::text[],
|
|
7476
|
+
jsonb_build_object(
|
|
7477
|
+
'uuid', 'id',
|
|
7478
|
+
'appointment_id', 'booking_id@appointments',
|
|
7479
|
+
'service_id', 'service_id@services',
|
|
7480
|
+
'service_name', 'name',
|
|
7481
|
+
'duration_minutes', 'duration_minutes',
|
|
7482
|
+
'price', 'price',
|
|
7483
|
+
'created_at', 'created_at',
|
|
7484
|
+
'updated_at', 'updated_at',
|
|
7485
|
+
'tenant_id', '-',
|
|
7486
|
+
'is_revision', 'jsonb:agenda.is_revision',
|
|
7487
|
+
'revision_number', 'jsonb:agenda.revision_number',
|
|
7488
|
+
'parent_journey_id', 'jsonb:agenda.v1_parent_journey_ref',
|
|
7489
|
+
'added_during_execution', 'jsonb:agenda.added_during_execution',
|
|
7490
|
+
'source_evaluation_execution_id','jsonb:agenda.v1_source_evaluation_ref'),
|
|
7491
|
+
'O servi\xE7o dentro do Compromisso. \`service_name\` n\xE3o existe no V1 \u2014 vem da '
|
|
7492
|
+
'ponte do extrator, porque o destino exige nome e a origem guarda o nome no '
|
|
7493
|
+
'servi\xE7o, n\xE3o na linha. O pre\xE7o \xE9 o pre\xE7o praticado NAQUELE agendamento, e '
|
|
7494
|
+
'por isso vem cru e n\xE3o da tabela de pre\xE7os.'
|
|
7495
|
+
);
|
|
7496
|
+
|
|
7497
|
+
DELETE FROM migration.excluded_tables WHERE source_table IN ('appointments', 'appointment_services');
|
|
7498
|
+
|
|
7499
|
+
RESET migration.allowlist_unlock;
|
|
7500
|
+
|
|
7501
|
+
|
|
7502
|
+
-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7503
|
+
--
|
|
7504
|
+
-- Fun\xE7\xE3o nova nasce execut\xE1vel por PUBLIC, e \`anon\`/\`authenticated\` herdam
|
|
7505
|
+
-- dali. Estas s\xE3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer
|
|
7506
|
+
-- inquilino. S\xF3 o service_role, que \xE9 quem roda a onda.
|
|
7507
|
+
REVOKE ALL ON FUNCTION migration.upsert_appointment(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;
|
|
7508
|
+
GRANT EXECUTE ON FUNCTION migration.upsert_appointment(uuid, uuid, jsonb, jsonb) TO service_role;
|
|
7509
|
+
REVOKE ALL ON FUNCTION migration.upsert_appointment_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;
|
|
7510
|
+
GRANT EXECUTE ON FUNCTION migration.upsert_appointment_item(uuid, uuid, jsonb, jsonb) TO service_role;
|
|
7511
|
+
`;
|
|
7512
|
+
var MIGRATION_009_O_EXECUTOR_SE_ACHA_PELO_PEDIDO = `-- O executor se acha pelo pedido.
|
|
7513
|
+
--
|
|
7514
|
+
-- \`trg_financial_seed_item_participant\` dispara em todo INSERT de
|
|
7515
|
+
-- \`public.items\` e pergunta uma coisa s\xF3:
|
|
7516
|
+
--
|
|
7517
|
+
-- SELECT a.assignee_id, a.unit_id
|
|
7518
|
+
-- FROM public.appointments a
|
|
7519
|
+
-- WHERE a.order_id = NEW.order_id AND a.assignee_id IS NOT NULL
|
|
7520
|
+
-- ORDER BY a.created_at LIMIT 1
|
|
7521
|
+
--
|
|
7522
|
+
-- \xC9 a pergunta certa \u2014 "quem executou o servi\xE7o desta linha?" \u2014, e \xE9 o que
|
|
7523
|
+
-- alimenta a comiss\xE3o. O que faltava era o \xEDndice que a responde:
|
|
7524
|
+
-- \`public.appointments\` tem dez \xEDndices e **nenhum por \`order_id\`**.
|
|
7525
|
+
--
|
|
7526
|
+
-- \u2500\u2500 Medido \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7527
|
+
--
|
|
7528
|
+
-- EXPLAIN ANALYZE de um INSERT em \`public.items\`, tenant Espa\xE7o Facial,
|
|
7529
|
+
-- 20/09/2026, com 179.613 agendamentos na tabela:
|
|
7530
|
+
--
|
|
7531
|
+
-- Trigger items_financial_seed_participant: time=56.799
|
|
7532
|
+
-- ... os outros dezesseis gatilhos somados: time= 5.268
|
|
7533
|
+
-- Execution Time: 62.067 ms
|
|
7534
|
+
--
|
|
7535
|
+
-- **91% do custo de inserir um item.** E o n\xFAmero cresce com a agenda: o mesmo
|
|
7536
|
+
-- INSERT vai custar o dobro quando os 354.208 compromissos do V1 tiverem
|
|
7537
|
+
-- entrado, porque a varredura \xE9 da tabela inteira.
|
|
7538
|
+
--
|
|
7539
|
+
-- \u2500\u2500 N\xE3o \xE9 s\xF3 da migra\xE7\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7540
|
+
--
|
|
7541
|
+
-- Vale registrar, porque o \xEDndice se paga duas vezes: este gatilho roda toda
|
|
7542
|
+
-- vez que algu\xE9m acrescenta uma linha a uma venda no app \u2014 a comanda, o
|
|
7543
|
+
-- carrinho, o atendimento que ganha um produto no fim. Numa rede com agenda
|
|
7544
|
+
-- cheia, cada item somava dezenas de milissegundos de varredura que ningu\xE9m
|
|
7545
|
+
-- pediu. O porte s\xF3 foi quem gritou primeiro: 220.462 itens a 62 ms s\xE3o quatro
|
|
7546
|
+
-- horas.
|
|
7547
|
+
--
|
|
7548
|
+
-- \u2500\u2500 Por que PARCIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7549
|
+
--
|
|
7550
|
+
-- O compromisso do V1 n\xE3o tem Venda: ele chega com \`order_id\` NULL, e s\xE3o
|
|
7551
|
+
-- centenas de milhares assim. Um \xEDndice completo carregaria todos eles para
|
|
7552
|
+
-- responder sobre os poucos que t\xEAm pedido. O parcial indexa s\xF3 as linhas que
|
|
7553
|
+
-- a consulta pode encontrar \u2014 e o planejador o usa porque o predicado
|
|
7554
|
+
-- (\`order_id = \u2026\`) implica a condi\xE7\xE3o do \xEDndice.
|
|
7555
|
+
--
|
|
7556
|
+
-- \`CONCURRENTLY\` porque o app est\xE1 no ar: a forma normal tranca a escrita em
|
|
7557
|
+
-- \`appointments\` enquanto constr\xF3i, e uma recep\xE7\xE3o marcando hor\xE1rio n\xE3o tem
|
|
7558
|
+
-- por que esperar por um \xEDndice.
|
|
7559
|
+
--
|
|
7560
|
+
-- Idempotente.
|
|
7561
|
+
|
|
7562
|
+
-- Sem CONCURRENTLY: \xE9 o \xFAnico arquivo do reposit\xF3rio que o usava, e ele n\xE3o
|
|
7563
|
+
-- roda dentro de transa\xE7\xE3o \u2014 o encadeamento aplica cada migra\xE7\xE3o numa. A
|
|
7564
|
+
-- tabela \xE9 pequena o bastante para o bloqueio curto de um CREATE INDEX comum.
|
|
7565
|
+
CREATE INDEX IF NOT EXISTS appointments_order_idx
|
|
7566
|
+
ON public.appointments (order_id)
|
|
7567
|
+
WHERE order_id IS NOT NULL;
|
|
7568
|
+
|
|
7569
|
+
COMMENT ON INDEX public.appointments_order_idx IS
|
|
7570
|
+
'O caminho de "quem executou o servi\xE7o desta linha da venda", que '
|
|
7571
|
+
'trg_financial_seed_item_participant percorre em todo INSERT de items. Sem '
|
|
7572
|
+
'ele a pergunta era varredura da agenda inteira: 56,8 ms dos 62 ms de '
|
|
7573
|
+
'inserir um item, medido com 179.613 compromissos.';
|
|
7574
|
+
`;
|
|
6930
7575
|
var MIGRATIONS = [
|
|
6931
7576
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
6932
7577
|
{ id: "001_the_connector_definers_keep_a_pinned_path", sql: MIGRATION_001_THE_CONNECTOR_DEFINERS_KEEP_A_PINNED_PATH },
|
|
@@ -6934,7 +7579,10 @@ var MIGRATIONS = [
|
|
|
6934
7579
|
{ id: "003_the_client_arrives_before_being_served", sql: MIGRATION_003_THE_CLIENT_ARRIVES_BEFORE_BEING_SERVED },
|
|
6935
7580
|
{ id: "004_the_appointment_carries_its_unit", sql: MIGRATION_004_THE_APPOINTMENT_CARRIES_ITS_UNIT },
|
|
6936
7581
|
{ id: "005_the_queue_knows_how_long_she_waited", sql: MIGRATION_005_THE_QUEUE_KNOWS_HOW_LONG_SHE_WAITED },
|
|
6937
|
-
{ id: "006_the_agenda_stops_pricing_every_invoice", sql: MIGRATION_006_THE_AGENDA_STOPS_PRICING_EVERY_INVOICE }
|
|
7582
|
+
{ id: "006_the_agenda_stops_pricing_every_invoice", sql: MIGRATION_006_THE_AGENDA_STOPS_PRICING_EVERY_INVOICE },
|
|
7583
|
+
{ id: "007_o_cascade_levou_a_agenda_junto", sql: MIGRATION_007_O_CASCADE_LEVOU_A_AGENDA_JUNTO },
|
|
7584
|
+
{ id: "008_a_agenda_do_v1_atravessa", sql: MIGRATION_008_A_AGENDA_DO_V1_ATRAVESSA },
|
|
7585
|
+
{ id: "009_o_executor_se_acha_pelo_pedido", sql: MIGRATION_009_O_EXECUTOR_SE_ACHA_PELO_PEDIDO }
|
|
6938
7586
|
];
|
|
6939
7587
|
function computeCommissionAmount(total, ratePercent) {
|
|
6940
7588
|
const base = Number(total) || 0;
|