@fayz-ai/plugin-fiscal-br 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { packagingCount } from './chunk-5LI2FDOM.js';
2
2
  import { createConnectionStore, recordRailFields, RECORD_GROUP, activeFacet, describeError, getSupabaseClientOptional, formatCurrency, maskCpfCnpj, connectionStatus, definePlugin, getActiveTenantId as getActiveTenantId$1 } from '@fayz-ai/core';
3
3
  import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
4
- import { ModalHeader, ModalTitle, ModalBody, Skeleton, AiActionButton, Dropdown, DropdownTrigger, Button, DropdownContent, DropdownItem, Hint, Card, InlineLink, SearchSelect, Input, EmBreve, DatePicker, ModalFooter, SubpageHeader, Modal, ModalContent, ModalDescription, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, CardContent, CardHeader, CardTitle, CardDescription, Badge, Switch, useFilterDraft, RowActions, PageHeaderActions, FilterPanel, FilterPanelField, SearchField, TooltipProvider, DataTable, ChevronPager, Tooltip, TooltipTrigger, TooltipContent, SegmentedControl, ComingSoon, toast } from '@fayz-ai/ui';
4
+ import { ModalHeader, ModalTitle, ModalBody, Skeleton, AiActionButton, Dropdown, DropdownTrigger, Button, DropdownContent, DropdownItem, Hint, Card, InlineLink, SearchSelect, Input, EmBreve, DatePicker, ModalFooter, SubpageHeader, Modal, ModalContent, ModalDescription, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, CardContent, CardHeader, CardTitle, CardDescription, Badge, Switch, useFilterDraft, RowActions, PageHeaderActions, FilterPanel, FilterPanelField, SearchField, TooltipProvider, DataTable, ChevronPager, Tooltip, TooltipTrigger, TooltipContent, SegmentedControl, toast, Textarea, ComingSoon } from '@fayz-ai/ui';
5
5
  export { ComingSoon, EmBreve } from '@fayz-ai/ui';
6
6
  import { Loader2, Download, ChevronDown, FileCode2, FileText, RefreshCw, UserPlus, Gift, AlertTriangle, ChevronRight, Check, Trash2, Plus, KeyRound, Image, UploadCloud, Paperclip, ScanLine, Eye, X, ShieldCheck, ListChecks, FileDown, CheckCircle2, FileCheck2, XCircle, Inbox, Info } from 'lucide-react';
7
- import { registerDocumentOpener, PersonLink, getActiveTenantId, useUnitFilter, PageActions, adminNavigateTo, usePermissionOptional, UNIT_DETAIL_ZONES, serviceBaseUrl } from '@fayz-ai/admin';
7
+ import { registerDocumentOpener, PersonLink, getActiveTenantId, useUnitFilter, PageActions, adminNavigateTo, usePermissionOptional, OrderDetailBlock, UNIT_DETAIL_ZONES, ORDER_DETAIL_ZONES, serviceBaseUrl } from '@fayz-ai/admin';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
  import { FINANCIAL_INVOICE_ZONES } from '@fayz-ai/financial/views/zones';
10
10
 
@@ -18,6 +18,56 @@ var EmitterError = class extends Error {
18
18
  this.name = "EmitterError";
19
19
  }
20
20
  };
21
+ async function send(method, path, body) {
22
+ const base = serviceBaseUrl();
23
+ const supabase = getSupabaseClientOptional();
24
+ if (!base || !supabase) throw new EmitterError(503, "no_client", "the integration service is not configured");
25
+ const { data } = await supabase.auth.getSession();
26
+ const token = data?.session?.access_token;
27
+ if (!token) throw new EmitterError(401, "unauthorized", "sign in again");
28
+ const tenant = getActiveTenantId$1();
29
+ try {
30
+ return await fetch(`${base}${path}`, {
31
+ method,
32
+ headers: {
33
+ authorization: `Bearer ${token}`,
34
+ ...tenant ? { "x-fayz-tenant": tenant } : {},
35
+ ...body === void 0 ? {} : { "content-type": "application/json" }
36
+ },
37
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
38
+ });
39
+ } catch (error) {
40
+ throw new EmitterError(503, "unavailable", error instanceof Error ? error.message : String(error));
41
+ }
42
+ }
43
+ async function refusal(response) {
44
+ const text = await response.text();
45
+ let payload = null;
46
+ try {
47
+ payload = text ? JSON.parse(text) : null;
48
+ } catch {
49
+ }
50
+ return new EmitterError(
51
+ response.status,
52
+ payload?.error ?? "unavailable",
53
+ payload?.message ?? text.slice(0, 200),
54
+ Array.isArray(payload?.gates) ? payload.gates : [],
55
+ typeof payload?.field === "string" ? payload.field : null
56
+ );
57
+ }
58
+ async function request(method, path, body) {
59
+ const response = await send(method, path, body);
60
+ if (!response.ok) throw await refusal(response);
61
+ const text = await response.text();
62
+ return text ? JSON.parse(text) : null;
63
+ }
64
+ async function requestFile(path) {
65
+ const response = await send("GET", path);
66
+ if (!response.ok) throw await refusal(response);
67
+ return response.blob();
68
+ }
69
+
70
+ // src/lib/emitters.ts
21
71
  var SENTENCES = {
22
72
  no_client: "O servi\xE7o de integra\xE7\xF5es n\xE3o est\xE1 configurado neste app.",
23
73
  not_configured: "A emiss\xE3o fiscal ainda n\xE3o est\xE1 ligada no servidor.",
@@ -71,43 +121,6 @@ var GATE_LABELS = {
71
121
  csc: "CSC (ID e c\xF3digo) da NFC-e",
72
122
  registered: "Empresa cadastrada na PlugNotas"
73
123
  };
74
- async function request(method, path, body) {
75
- const base = serviceBaseUrl();
76
- const supabase = getSupabaseClientOptional();
77
- if (!base || !supabase) throw new EmitterError(503, "no_client", "the integration service is not configured");
78
- const { data } = await supabase.auth.getSession();
79
- const token = data?.session?.access_token;
80
- if (!token) throw new EmitterError(401, "unauthorized", "sign in again");
81
- const tenant = getActiveTenantId$1();
82
- let response;
83
- try {
84
- response = await fetch(`${base}${path}`, {
85
- method,
86
- headers: {
87
- authorization: `Bearer ${token}`,
88
- ...tenant ? { "x-fayz-tenant": tenant } : {},
89
- ...body === void 0 ? {} : { "content-type": "application/json" }
90
- },
91
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
92
- });
93
- } catch (error) {
94
- throw new EmitterError(503, "unavailable", error instanceof Error ? error.message : String(error));
95
- }
96
- const text = await response.text();
97
- let payload = null;
98
- try {
99
- payload = text ? JSON.parse(text) : null;
100
- } catch {
101
- }
102
- if (response.ok) return payload;
103
- throw new EmitterError(
104
- response.status,
105
- payload?.error ?? "unavailable",
106
- payload?.message ?? text.slice(0, 200),
107
- Array.isArray(payload?.gates) ? payload.gates : [],
108
- typeof payload?.field === "string" ? payload.field : null
109
- );
110
- }
111
124
  var one = (legalEntityId, rest = "") => `/v1/fiscal/emitters/${encodeURIComponent(legalEntityId)}${rest}`;
112
125
  async function base64Of(file) {
113
126
  const bytes = new Uint8Array(await file.arrayBuffer());
@@ -363,7 +376,7 @@ function Certificate({ emitter, onChange }) {
363
376
  const [password, setPassword] = React.useState("");
364
377
  const [sending, setSending] = React.useState(false);
365
378
  const c = emitter.certificate;
366
- const send = async () => {
379
+ const send2 = async () => {
367
380
  if (!file) return;
368
381
  setSending(true);
369
382
  try {
@@ -395,7 +408,7 @@ function Certificate({ emitter, onChange }) {
395
408
  ] }) }),
396
409
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
397
410
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "O arquivo vai direto para a PlugNotas e n\xE3o fica guardado no nosso sistema." }),
398
- /* @__PURE__ */ jsx(Button, { size: "sm", disabled: !file || !password || sending, onClick: () => void send(), children: sending ? "Enviando\u2026" : c ? "Trocar certificado" : "Enviar certificado" })
411
+ /* @__PURE__ */ jsx(Button, { size: "sm", disabled: !file || !password || sending, onClick: () => void send2(), children: sending ? "Enviando\u2026" : c ? "Trocar certificado" : "Enviar certificado" })
399
412
  ] })
400
413
  ] });
401
414
  }
@@ -4907,6 +4920,315 @@ BEGIN
4907
4920
  END
4908
4921
  $$;
4909
4922
  `;
4923
+ var MIGRATION_031_THE_SERVICE_ISSUES_THE_NOTE = `-- ============================================================================
4924
+ -- 031 \u2014 the integration service issues the order's note
4925
+ --
4926
+ -- control-sdk-backend ADR 0008, step 2: an operator clicks "Emitir NFC-e" on
4927
+ -- an order and the integration service builds the note with the V1's rules,
4928
+ -- sends it to PlugNotas and records what comes back. The note is the record
4929
+ -- 005 already defined (public.documents + plg_fiscalbr_documents, linked to
4930
+ -- the order by plg_fiscalbr_document_orders); these are the service's doors to
4931
+ -- it, one function per action, the tenant explicit, open only to
4932
+ -- \`fayz_integration\`.
4933
+ --
4934
+ -- fiscalbr_order_for_note_for_service what the note is made of
4935
+ -- fiscalbr_note_open_for_service the note, opened before the call
4936
+ -- fiscalbr_note_apply_for_service what PlugNotas answered
4937
+ -- fiscalbr_notes_for_service an order's notes
4938
+ -- fiscalbr_note_for_service one note
4939
+ -- fiscalbr_notes_to_follow_for_service notes still waiting on PlugNotas
4940
+ --
4941
+ -- A note is opened BEFORE PlugNotas is called, with the idIntegracao it will
4942
+ -- carry, so a call whose answer is lost is found again by that id instead of
4943
+ -- being sent twice (the V1's FISCAL_PROVIDER_RESULT_UNCERTAIN rule).
4944
+ -- ============================================================================
4945
+
4946
+ -- What the note of an order is made of: the order, its lines with their tax
4947
+ -- data, what was paid and how, who bought and where it goes. The service
4948
+ -- applies the rules; this only gathers.
4949
+ CREATE OR REPLACE FUNCTION public.fiscalbr_order_for_note_for_service(p_tenant uuid, p_order uuid)
4950
+ RETURNS jsonb
4951
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
4952
+ SET search_path TO ''
4953
+ AS $$
4954
+ DECLARE
4955
+ v_checkout jsonb;
4956
+ BEGIN
4957
+ -- The shop is optional: a site order says how it was paid on its checkout,
4958
+ -- read only where the shop's table exists.
4959
+ IF to_regclass('public.plg_shop_checkout_sessions') IS NOT NULL THEN
4960
+ EXECUTE $q$
4961
+ SELECT jsonb_build_object(
4962
+ 'paymentMethod', s.payment_method, 'provider', s.provider, 'total', s.total, 'status', s.status,
4963
+ 'customerName', s.customer_name, 'customerDocument', s.customer_document,
4964
+ 'destinationState', s.shipping_address ->> 'state', 'destinationCity', s.shipping_address ->> 'city')
4965
+ FROM public.plg_shop_checkout_sessions s
4966
+ WHERE s.order_id = $1 AND s.tenant_id = $2 AND s.status = 'paid'
4967
+ ORDER BY s.updated_at DESC LIMIT 1 $q$
4968
+ INTO v_checkout USING p_order, p_tenant;
4969
+ END IF;
4970
+
4971
+ RETURN (SELECT jsonb_build_object(
4972
+ 'orderId', o.id,
4973
+ 'tenantId', o.tenant_id,
4974
+ 'unitId', o.unit_id,
4975
+ 'kind', o.kind,
4976
+ 'status', o.status,
4977
+ 'channel', o.channel,
4978
+ 'createdAt', o.created_at,
4979
+ 'subtotal', o.subtotal,
4980
+ 'discount', o.discount,
4981
+ 'shipping', o.shipping,
4982
+ 'total', o.total,
4983
+ 'legalEntityId', u.company_legal_entity_id,
4984
+ 'items', coalesce((
4985
+ SELECT jsonb_agg(jsonb_build_object(
4986
+ 'itemId', i.id,
4987
+ 'productId', i.product_id,
4988
+ 'code', coalesce(nullif(p.sku, ''), i.product_id::text, i.id::text),
4989
+ 'name', i.name,
4990
+ 'quantity', i.quantity,
4991
+ 'unitPrice', i.unit_price,
4992
+ 'discount', i.discount,
4993
+ 'total', i.total,
4994
+ 'cancelled', i.cancelled_at IS NOT NULL,
4995
+ -- The product's own value wins, field by field, over its group's (V1).
4996
+ 'tax', CASE WHEN t.product_id IS NULL AND g.id IS NULL THEN NULL ELSE jsonb_build_object(
4997
+ 'ncm', t.ncm,
4998
+ 'cest', coalesce(t.cest, g.cest),
4999
+ 'cfop', coalesce(nullif(t.cfop, ''), g.cfop),
5000
+ 'cst', coalesce(nullif(t.cst, ''), g.cst),
5001
+ 'csosn', g.csosn,
5002
+ 'origin', coalesce(nullif(t.icms_origin, ''), g.icms_origin),
5003
+ 'icmsRate', CASE WHEN coalesce(t.icms_rate, 0) > 0 THEN t.icms_rate ELSE g.icms_rate END,
5004
+ 'icmsBaseMode', g.icms_bc_mode,
5005
+ 'pisSituation', coalesce(nullif(t.pis_situation, ''), g.pis_situation),
5006
+ 'pisRate', CASE WHEN coalesce(t.pis_rate, 0) > 0 THEN t.pis_rate ELSE g.pis_rate END,
5007
+ 'cofinsSituation', coalesce(nullif(t.cofins_situation, ''), g.cofins_situation),
5008
+ 'cofinsRate', CASE WHEN coalesce(t.cofins_rate, 0) > 0 THEN t.cofins_rate ELSE g.cofins_rate END,
5009
+ 'ibsCbsSituation', coalesce(nullif(t.ibs_cbs_situation, ''), g.ibs_cbs_situation),
5010
+ 'ibsCbsClassification', coalesce(nullif(t.ibs_cbs_classification, ''), g.ibs_cbs_classification),
5011
+ 'group', g.name
5012
+ ) END
5013
+ ) ORDER BY i.sort_order, i.created_at)
5014
+ FROM public.items i
5015
+ LEFT JOIN public.products p ON p.id = i.product_id AND p.tenant_id = i.tenant_id
5016
+ LEFT JOIN public.plg_fiscalbr_product_tax_info t ON t.product_id = i.product_id AND t.tenant_id = i.tenant_id
5017
+ LEFT JOIN public.plg_fiscalbr_tax_groups g ON g.id = t.tax_group_id AND g.tenant_id = i.tenant_id
5018
+ WHERE i.order_id = o.id AND i.tenant_id = o.tenant_id
5019
+ ), '[]'::jsonb),
5020
+ -- Anything that is not a site order is paid through the movements the
5021
+ -- order received, net of reversals, by method.
5022
+ 'checkout', v_checkout,
5023
+ 'payments', coalesce((
5024
+ SELECT jsonb_agg(jsonb_build_object('method', x.method, 'methodType', x.method_type, 'amount', x.amount))
5025
+ FROM (
5026
+ SELECT pm.name AS method, pt.name AS method_type,
5027
+ sum(CASE WHEN m.direction = 'credit' THEN m.paid_amount ELSE -m.paid_amount END) AS amount
5028
+ FROM public.plg_financial_movements m
5029
+ LEFT JOIN public.plg_financial_payment_methods pm ON pm.id = m.payment_method_id
5030
+ LEFT JOIN public.plg_financial_payment_method_types pt ON pt.id = pm.payment_method_type_id
5031
+ WHERE m.tenant_id = o.tenant_id AND m.movement_kind = 'payment' AND m.status = 'paid'
5032
+ AND (m.invoice_id = o.id OR m.financial_invoice_id IN (
5033
+ SELECT fi.id FROM public.plg_financial_invoices fi WHERE fi.order_id = o.id AND fi.tenant_id = o.tenant_id))
5034
+ GROUP BY pm.name, pt.name
5035
+ ) x WHERE x.amount <> 0
5036
+ ), '[]'::jsonb),
5037
+ 'notes', coalesce((
5038
+ SELECT jsonb_agg(jsonb_build_object('documentId', d.document_id, 'model', d.model, 'status', d.status))
5039
+ FROM public.plg_fiscalbr_documents d
5040
+ WHERE d.tenant_id = o.tenant_id AND d.order_id = o.id
5041
+ ), '[]'::jsonb)
5042
+ )
5043
+ FROM public.orders o
5044
+ LEFT JOIN app.units u ON u.id = o.unit_id AND u.tenant_id = o.tenant_id
5045
+ WHERE o.id = p_order AND o.tenant_id = p_tenant);
5046
+ END
5047
+ $$;
5048
+
5049
+ -- The note, opened before PlugNotas is called. One live note per order and
5050
+ -- model: a second "Emitir" while one is processing or authorized is refused
5051
+ -- (P0001 note_exists), so a double click never issues twice.
5052
+ CREATE OR REPLACE FUNCTION public.fiscalbr_note_open_for_service(
5053
+ p_tenant uuid, p_order uuid, p_model text, p_input jsonb
5054
+ )
5055
+ RETURNS jsonb
5056
+ LANGUAGE plpgsql SECURITY DEFINER
5057
+ SET search_path TO ''
5058
+ AS $$
5059
+ DECLARE
5060
+ v_order public.orders%ROWTYPE;
5061
+ v_doc uuid := gen_random_uuid();
5062
+ v_integration text;
5063
+ v_existing uuid;
5064
+ BEGIN
5065
+ IF p_model NOT IN ('nfce', 'nfe') THEN
5066
+ RAISE EXCEPTION 'fiscalbr_note_open: model % is not issued here', p_model USING ERRCODE = '22023';
5067
+ END IF;
5068
+ SELECT * INTO v_order FROM public.orders WHERE id = p_order AND tenant_id = p_tenant FOR UPDATE;
5069
+ IF NOT FOUND THEN
5070
+ RAISE EXCEPTION 'fiscalbr_note_open: order % is not of tenant %', p_order, p_tenant USING ERRCODE = 'P0002';
5071
+ END IF;
5072
+ SELECT document_id INTO v_existing FROM public.plg_fiscalbr_documents
5073
+ WHERE tenant_id = p_tenant AND order_id = p_order AND model = p_model AND status IN ('processing', 'authorized')
5074
+ LIMIT 1;
5075
+ IF v_existing IS NOT NULL THEN
5076
+ RAISE EXCEPTION 'note_exists' USING ERRCODE = 'P0001', DETAIL = v_existing::text;
5077
+ END IF;
5078
+
5079
+ -- Ours, and unique per company at PlugNotas: how a lost answer is found again.
5080
+ v_integration := 'csb-' || v_doc::text;
5081
+
5082
+ INSERT INTO public.documents (id, tenant_id, kind, title, status, unit_id, subject_type, subject_id, created_by, metadata)
5083
+ VALUES (v_doc, p_tenant, 'fiscal_document', upper(p_model) || ' do pedido', 'processing', v_order.unit_id,
5084
+ 'order', p_order, nullif(p_input ->> 'userId', '')::uuid, jsonb_build_object('source', 'integration-service'));
5085
+
5086
+ INSERT INTO public.plg_fiscalbr_documents (
5087
+ document_id, tenant_id, unit_id, order_id, model, environment, status, series, provider,
5088
+ provider_integration_id, total_value, recipient_snapshot, request_payload, source_snapshot, emitted_by_user_id, metadata)
5089
+ VALUES (
5090
+ v_doc, p_tenant, v_order.unit_id, p_order, p_model, p_input ->> 'environment', 'processing', p_input ->> 'series', 'plugnotas',
5091
+ v_integration, (p_input ->> 'total')::numeric, p_input -> 'recipient', p_input -> 'request', p_input -> 'source',
5092
+ nullif(p_input ->> 'userId', '')::uuid, jsonb_build_object('emitterCnpj', p_input ->> 'emitterCnpj'));
5093
+
5094
+ INSERT INTO public.plg_fiscalbr_document_orders (tenant_id, document_id, order_id)
5095
+ VALUES (p_tenant, v_doc, p_order);
5096
+
5097
+ INSERT INTO public.plg_fiscalbr_document_events (tenant_id, unit_id, document_id, event_type, status, created_by_user_id)
5098
+ VALUES (p_tenant, v_order.unit_id, v_doc, 'issue_requested', 'processing', nullif(p_input ->> 'userId', '')::uuid);
5099
+
5100
+ RETURN jsonb_build_object('documentId', v_doc, 'integrationId', v_integration);
5101
+ END
5102
+ $$;
5103
+
5104
+ -- What PlugNotas answered, on the note and on its public.documents row (the
5105
+ -- same note seen from two places, 005). Identity (model, unit, order) never moves.
5106
+ CREATE OR REPLACE FUNCTION public.fiscalbr_note_apply_for_service(
5107
+ p_tenant uuid, p_document uuid, p_patch jsonb, p_event jsonb DEFAULT NULL
5108
+ )
5109
+ RETURNS void
5110
+ LANGUAGE plpgsql SECURITY DEFINER
5111
+ SET search_path TO ''
5112
+ AS $$
5113
+ DECLARE
5114
+ v public.plg_fiscalbr_documents%ROWTYPE;
5115
+ v_status text := p_patch ->> 'status';
5116
+ BEGIN
5117
+ SELECT * INTO v FROM public.plg_fiscalbr_documents
5118
+ WHERE document_id = p_document AND tenant_id = p_tenant FOR UPDATE;
5119
+ IF NOT FOUND THEN
5120
+ RAISE EXCEPTION 'fiscalbr_note_apply: note % is not of tenant %', p_document, p_tenant USING ERRCODE = 'P0002';
5121
+ END IF;
5122
+ IF v_status IS NOT NULL AND v_status NOT IN ('processing', 'authorized', 'rejected', 'error', 'cancelled', 'substituted') THEN
5123
+ RAISE EXCEPTION 'fiscalbr_note_apply: unknown status %', v_status USING ERRCODE = '22023';
5124
+ END IF;
5125
+
5126
+ UPDATE public.plg_fiscalbr_documents SET
5127
+ status = coalesce(v_status, status),
5128
+ number = coalesce(p_patch ->> 'number', number),
5129
+ series = coalesce(p_patch ->> 'series', series),
5130
+ access_key = coalesce(p_patch ->> 'access_key', access_key),
5131
+ protocol = coalesce(p_patch ->> 'protocol', protocol),
5132
+ provider_document_id = coalesce(p_patch ->> 'provider_document_id', provider_document_id),
5133
+ response_payload = coalesce(p_patch -> 'response_payload', response_payload),
5134
+ status_payload = coalesce(p_patch -> 'status_payload', status_payload),
5135
+ rejection_payload = CASE WHEN p_patch ? 'rejection_payload' THEN p_patch -> 'rejection_payload' ELSE rejection_payload END,
5136
+ xml = coalesce(p_patch ->> 'xml', xml),
5137
+ authorized_at = coalesce((p_patch ->> 'authorized_at')::timestamptz, authorized_at),
5138
+ last_polled_at = CASE WHEN p_patch ? 'polled' THEN now() ELSE last_polled_at END,
5139
+ cancelled_at = CASE WHEN p_patch ? 'cancelled_at' THEN (p_patch ->> 'cancelled_at')::timestamptz ELSE cancelled_at END,
5140
+ cancel_reason = CASE WHEN p_patch ? 'cancel_reason' THEN p_patch ->> 'cancel_reason' ELSE cancel_reason END,
5141
+ cancelled_by_user_id = CASE WHEN p_patch ? 'cancelled_by_user_id' THEN nullif(p_patch ->> 'cancelled_by_user_id', '')::uuid ELSE cancelled_by_user_id END,
5142
+ metadata = metadata || coalesce(p_patch -> 'metadata', '{}'::jsonb)
5143
+ WHERE document_id = p_document;
5144
+
5145
+ IF v_status IS NOT NULL THEN
5146
+ UPDATE public.documents SET status = v_status WHERE id = p_document AND status IS DISTINCT FROM v_status;
5147
+ END IF;
5148
+
5149
+ IF p_event IS NOT NULL THEN
5150
+ INSERT INTO public.plg_fiscalbr_document_events
5151
+ (tenant_id, unit_id, document_id, event_type, status, justification, response_payload, created_by_user_id)
5152
+ VALUES (v.tenant_id, v.unit_id, p_document, p_event ->> 'event_type', p_event ->> 'status',
5153
+ p_event ->> 'justification', p_event -> 'response_payload', nullif(p_event ->> 'userId', '')::uuid);
5154
+ END IF;
5155
+ END
5156
+ $$;
5157
+
5158
+ CREATE OR REPLACE FUNCTION public.fiscalbr_note_for_service(p_tenant uuid, p_document uuid)
5159
+ RETURNS jsonb
5160
+ LANGUAGE sql STABLE SECURITY DEFINER
5161
+ SET search_path TO ''
5162
+ AS $$
5163
+ SELECT jsonb_build_object(
5164
+ 'documentId', d.document_id, 'tenantId', d.tenant_id, 'unitId', d.unit_id, 'orderId', d.order_id,
5165
+ 'model', d.model, 'environment', d.environment, 'status', d.status, 'series', d.series, 'number', d.number,
5166
+ 'accessKey', d.access_key, 'protocol', d.protocol, 'provider', d.provider,
5167
+ 'providerDocumentId', d.provider_document_id, 'integrationId', d.provider_integration_id,
5168
+ 'total', d.total_value, 'recipient', d.recipient_snapshot, 'rejection', d.rejection_payload,
5169
+ 'hasXml', d.xml IS NOT NULL, 'authorizedAt', d.authorized_at, 'lastPolledAt', d.last_polled_at,
5170
+ 'cancelledAt', d.cancelled_at, 'cancelReason', d.cancel_reason, 'createdAt', d.created_at,
5171
+ 'emitterCnpj', d.metadata ->> 'emitterCnpj', 'metadata', d.metadata)
5172
+ FROM public.plg_fiscalbr_documents d
5173
+ WHERE d.document_id = p_document AND d.tenant_id = p_tenant
5174
+ $$;
5175
+
5176
+ CREATE OR REPLACE FUNCTION public.fiscalbr_notes_for_service(p_tenant uuid, p_order uuid)
5177
+ RETURNS SETOF jsonb
5178
+ LANGUAGE sql STABLE SECURITY DEFINER
5179
+ SET search_path TO ''
5180
+ AS $$
5181
+ SELECT public.fiscalbr_note_for_service(p_tenant, d.document_id)
5182
+ FROM public.plg_fiscalbr_documents d
5183
+ WHERE d.tenant_id = p_tenant AND d.order_id = p_order
5184
+ ORDER BY d.created_at DESC
5185
+ $$;
5186
+
5187
+ -- The XML is large and read only on download.
5188
+ CREATE OR REPLACE FUNCTION public.fiscalbr_note_xml_for_service(p_tenant uuid, p_document uuid)
5189
+ RETURNS text
5190
+ LANGUAGE sql STABLE SECURITY DEFINER
5191
+ SET search_path TO ''
5192
+ AS $$
5193
+ SELECT d.xml FROM public.plg_fiscalbr_documents d WHERE d.document_id = p_document AND d.tenant_id = p_tenant
5194
+ $$;
5195
+
5196
+ -- Notes this service sent that PlugNotas has not settled: the fallback when a
5197
+ -- webhook does not arrive. Oldest poll first.
5198
+ CREATE OR REPLACE FUNCTION public.fiscalbr_notes_to_follow_for_service(p_limit integer DEFAULT 25)
5199
+ RETURNS SETOF jsonb
5200
+ LANGUAGE sql STABLE SECURITY DEFINER
5201
+ SET search_path TO ''
5202
+ AS $$
5203
+ SELECT public.fiscalbr_note_for_service(d.tenant_id, d.document_id)
5204
+ FROM public.plg_fiscalbr_documents d
5205
+ WHERE d.provider = 'plugnotas' AND d.status = 'processing' AND d.provider_integration_id LIKE 'csb-%'
5206
+ AND d.created_at > now() - interval '7 days'
5207
+ ORDER BY coalesce(d.last_polled_at, d.created_at)
5208
+ LIMIT greatest(1, least(p_limit, 100))
5209
+ $$;
5210
+
5211
+ DO $$
5212
+ DECLARE
5213
+ f text;
5214
+ BEGIN
5215
+ FOREACH f IN ARRAY ARRAY[
5216
+ 'public.fiscalbr_order_for_note_for_service(uuid, uuid)',
5217
+ 'public.fiscalbr_note_open_for_service(uuid, uuid, text, jsonb)',
5218
+ 'public.fiscalbr_note_apply_for_service(uuid, uuid, jsonb, jsonb)',
5219
+ 'public.fiscalbr_note_for_service(uuid, uuid)',
5220
+ 'public.fiscalbr_notes_for_service(uuid, uuid)',
5221
+ 'public.fiscalbr_note_xml_for_service(uuid, uuid)',
5222
+ 'public.fiscalbr_notes_to_follow_for_service(integer)'
5223
+ ] LOOP
5224
+ EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', f);
5225
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN
5226
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO fayz_integration', f);
5227
+ END IF;
5228
+ END LOOP;
5229
+ END
5230
+ $$;
5231
+ `;
4910
5232
  var MIGRATIONS = [
4911
5233
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
4912
5234
  { id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
@@ -4938,7 +5260,8 @@ var MIGRATIONS = [
4938
5260
  { id: "027_a_nota_com_conta_e_sem_estoque_termina_no_v2", sql: MIGRATION_027_A_NOTA_COM_CONTA_E_SEM_ESTOQUE_TERMINA_NO_V2 },
4939
5261
  { id: "028_a_linha_de_estoque_tem_unidade", sql: MIGRATION_028_A_LINHA_DE_ESTOQUE_TEM_UNIDADE },
4940
5262
  { id: "029_the_service_writes_the_intake_health", sql: MIGRATION_029_THE_SERVICE_WRITES_THE_INTAKE_HEALTH },
4941
- { id: "030_the_service_onboards_the_emitter", sql: MIGRATION_030_THE_SERVICE_ONBOARDS_THE_EMITTER }
5263
+ { id: "030_the_service_onboards_the_emitter", sql: MIGRATION_030_THE_SERVICE_ONBOARDS_THE_EMITTER },
5264
+ { id: "031_the_service_issues_the_note", sql: MIGRATION_031_THE_SERVICE_ISSUES_THE_NOTE }
4942
5265
  ];
4943
5266
  var taxGroupEntity = {
4944
5267
  name: "Grupo fiscal",
@@ -8396,6 +8719,65 @@ function UnitFiscalWidget({ item }) {
8396
8719
  ] })
8397
8720
  ] });
8398
8721
  }
8722
+
8723
+ // src/lib/notes.ts
8724
+ var CANCEL_MIN_JUSTIFICATION = 15;
8725
+ var SENTENCES2 = {
8726
+ no_client: "O servi\xE7o de integra\xE7\xF5es n\xE3o est\xE1 configurado neste app.",
8727
+ not_configured: "A emiss\xE3o fiscal ainda n\xE3o est\xE1 ligada no servidor.",
8728
+ unauthorized: "Sua sess\xE3o expirou. Entre de novo.",
8729
+ not_allowed: "Seu acesso n\xE3o permite emitir ou cancelar notas.",
8730
+ unavailable: "N\xE3o foi poss\xEDvel falar com o servi\xE7o fiscal. Tente de novo em instantes.",
8731
+ unknown_order: "Este pedido n\xE3o foi encontrado.",
8732
+ unknown_note: "Esta nota n\xE3o foi encontrada.",
8733
+ not_a_sale: "S\xF3 uma venda recebe NFC-e.",
8734
+ order_not_closed: "Este pedido ainda n\xE3o pode receber nota.",
8735
+ no_emitter: "A unidade deste pedido n\xE3o tem empresa (CNPJ) cadastrada para emitir.",
8736
+ emitter_not_owned: "Esta empresa ainda emite pelo sistema anterior. Configure em Integra\xE7\xF5es \u2192 PlugNotas.",
8737
+ emitter_not_ready: "Falta configurar a empresa para emitir NFC-e (Integra\xE7\xF5es \u2192 PlugNotas).",
8738
+ interstate: "Este pedido vai para outro estado: precisa de NF-e, n\xE3o de NFC-e.",
8739
+ no_series: "A empresa n\xE3o tem s\xE9rie de NFC-e configurada.",
8740
+ no_items: "O pedido n\xE3o tem produtos para a nota.",
8741
+ products_without_ncm: "H\xE1 produtos sem cadastro fiscal (NCM).",
8742
+ icms_code_mismatch: "H\xE1 produtos com c\xF3digo de ICMS que n\xE3o combina com o regime da empresa.",
8743
+ not_paid: "O pedido n\xE3o tem pagamento recebido para a nota.",
8744
+ totals_differ: "Os itens e os pagamentos do pedido n\xE3o fecham no mesmo total.",
8745
+ invalid_text: "Um produto tem um nome que a SEFAZ n\xE3o aceita (s\xF3 emojis ou caracteres especiais).",
8746
+ note_exists: "Este pedido j\xE1 tem uma nota emitida ou em processamento.",
8747
+ not_authorized: "S\xF3 uma nota autorizada pode ser cancelada ou baixada.",
8748
+ justification_too_short: `A justificativa precisa ter pelo menos ${CANCEL_MIN_JUSTIFICATION} caracteres.`,
8749
+ cancel_deadline_passed: "O prazo de cancelamento desta nota j\xE1 passou.",
8750
+ cancel_refused: "A SEFAZ recusou o cancelamento.",
8751
+ plugnotas_refused: "A PlugNotas recusou o envio."
8752
+ };
8753
+ function noteErrorSentence(error) {
8754
+ if (error instanceof EmitterError) {
8755
+ const base = SENTENCES2[error.code] ?? error.message;
8756
+ return error.code === "products_without_ncm" || error.code === "icms_code_mismatch" ? `${base} ${error.message.split(": ").slice(1).join(": ")}`.trim() : base;
8757
+ }
8758
+ return error instanceof Error ? error.message : String(error);
8759
+ }
8760
+ var order = (orderId) => `/v1/fiscal/orders/${encodeURIComponent(orderId)}/notes`;
8761
+ var one2 = (documentId, rest = "") => `/v1/fiscal/notes/${encodeURIComponent(documentId)}${rest}`;
8762
+ var orderNotes = {
8763
+ list: async (orderId) => (await request("GET", order(orderId))).notes,
8764
+ issueNfce: (orderId) => request("POST", order(orderId), { model: "nfce" }),
8765
+ /** Reads PlugNotas again while the note is processing. */
8766
+ read: (documentId) => request("GET", one2(documentId)),
8767
+ cancel: (documentId, justification) => request("PUT", one2(documentId, "/cancellation"), { justification }),
8768
+ pdf: (documentId) => requestFile(one2(documentId, "/pdf")),
8769
+ xml: (documentId) => requestFile(one2(documentId, "/xml"))
8770
+ };
8771
+ var NOTE_STATUS_LABEL = {
8772
+ processing: "Processando",
8773
+ authorized: "Autorizada",
8774
+ rejected: "Rejeitada",
8775
+ error: "Erro",
8776
+ cancelled: "Cancelada",
8777
+ substituted: "Substitu\xEDda"
8778
+ };
8779
+
8780
+ // src/data/issued.ts
8399
8781
  function sb3() {
8400
8782
  const supabase = getSupabaseClientOptional();
8401
8783
  if (!supabase) throw new Error("Supabase not initialized");
@@ -8457,35 +8839,26 @@ async function listNoteEvents(documentId) {
8457
8839
  createdAt: e.created_at
8458
8840
  }));
8459
8841
  }
8460
- async function callIssuer(body) {
8461
- const { data, error } = await sb3().functions.invoke("fiscalbr-plugnotas", { body: { ...body, tenantId: getActiveTenantId() } });
8462
- if (error) {
8463
- const ctx = error?.context;
8464
- let message = error.message || "Falha ao falar com o emissor fiscal.";
8465
- if (ctx && typeof ctx.json === "function") {
8466
- try {
8467
- const b = await ctx.json();
8468
- if (b?.error) message = String(b.error);
8469
- } catch {
8470
- }
8471
- }
8472
- throw new Error(message);
8473
- }
8474
- if (data?.error) throw new Error(String(data.error));
8475
- return data;
8476
- }
8477
8842
  async function refreshNote(documentId) {
8478
- const r = await callIssuer({ action: "poll_status", document_id: documentId });
8479
- return String(r?.status ?? "");
8843
+ try {
8844
+ return (await orderNotes.read(documentId)).status;
8845
+ } catch (error) {
8846
+ throw new Error(noteErrorSentence(error));
8847
+ }
8480
8848
  }
8481
- async function cancelNote(documentId, justification, code) {
8482
- const r = await callIssuer({ action: "cancel", document_id: documentId, justification, code });
8483
- return String(r?.status ?? "");
8849
+ async function cancelNote(documentId, justification) {
8850
+ try {
8851
+ return (await orderNotes.cancel(documentId, justification)).status;
8852
+ } catch (error) {
8853
+ throw new Error(noteErrorSentence(error));
8854
+ }
8484
8855
  }
8485
8856
  async function fetchNotePdf(documentId) {
8486
- const data = await callIssuer({ action: "get_pdf", document_id: documentId });
8487
- if (!(data instanceof Blob)) throw new Error("O emissor n\xE3o devolveu um PDF.");
8488
- return new Blob([data], { type: "application/pdf" });
8857
+ try {
8858
+ return await orderNotes.pdf(documentId);
8859
+ } catch (error) {
8860
+ throw new Error(noteErrorSentence(error));
8861
+ }
8489
8862
  }
8490
8863
 
8491
8864
  // src/lib/issued-note.ts
@@ -8664,7 +9037,7 @@ function NoteRow({ note, canCancel, onChanged }) {
8664
9037
  const [cancelling, setCancelling] = useState(false);
8665
9038
  const [busy, setBusy] = useState(null);
8666
9039
  const dimmed = isWithoutEffect(note.status);
8667
- const tone = (STATUS_META[note.status] ?? STATUS_META.error).tone;
9040
+ const tone2 = (STATUS_META[note.status] ?? STATUS_META.error).tone;
8668
9041
  const usable = note.status === "authorized" || note.status === "cancelled";
8669
9042
  const inFlight = note.status === "processing" || note.status === "draft";
8670
9043
  const cancellable = canCancel && note.status === "authorized" && !note.cancelledAt;
@@ -8712,7 +9085,7 @@ function NoteRow({ note, canCancel, onChanged }) {
8712
9085
  "n\xBA ",
8713
9086
  note.number
8714
9087
  ] }),
8715
- /* @__PURE__ */ jsx(Badge, { variant: TONE_VARIANT[tone], children: statusLabel(note) }),
9088
+ /* @__PURE__ */ jsx(Badge, { variant: TONE_VARIANT[tone2], children: statusLabel(note) }),
8716
9089
  note.environment === "homologacao" && /* @__PURE__ */ jsx("span", { className: "rounded border border-destructive px-1 text-[10px] font-semibold text-destructive", children: "TESTE" }),
8717
9090
  /* @__PURE__ */ jsx("span", { className: "ml-auto text-xs tabular-nums text-muted-foreground", children: dateTime(note.createdAt) })
8718
9091
  ] }),
@@ -8765,7 +9138,7 @@ function CancelNotePanel({ note, onClose, onDone }) {
8765
9138
  setError(null);
8766
9139
  setWorking("sending");
8767
9140
  try {
8768
- let status = await cancelNote(note.documentId, text.trim(), isNfse ? code : void 0);
9141
+ let status = await cancelNote(note.documentId, text.trim());
8769
9142
  setWorking("waiting");
8770
9143
  for (const delay of POLL_DELAYS) {
8771
9144
  if (status === "cancelled" || status === "authorized") break;
@@ -8891,6 +9264,184 @@ function NoteDetails({ note }) {
8891
9264
  ] })
8892
9265
  ] });
8893
9266
  }
9267
+ var POLL_DELAYS2 = [3, 5, 8, 12, 20, 20];
9268
+ var tone = (status) => status === "authorized" ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : status === "rejected" || status === "error" ? "border-destructive/40 bg-destructive/10 text-destructive" : "text-muted-foreground";
9269
+ async function openFile(blob, fileName, download) {
9270
+ const url = URL.createObjectURL(blob);
9271
+ if (download) {
9272
+ const a = document.createElement("a");
9273
+ a.href = url;
9274
+ a.download = fileName;
9275
+ a.click();
9276
+ } else {
9277
+ window.open(url, "_blank", "noopener");
9278
+ }
9279
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
9280
+ }
9281
+ function OrderNoteCard({ orderId }) {
9282
+ const [notes, setNotes] = React.useState(null);
9283
+ const [error, setError] = React.useState(null);
9284
+ const [issuing, setIssuing] = React.useState(false);
9285
+ const [cancelling, setCancelling] = React.useState(null);
9286
+ const load = React.useCallback(async () => {
9287
+ if (!orderId) return;
9288
+ try {
9289
+ setNotes(await orderNotes.list(orderId));
9290
+ setError(null);
9291
+ } catch (e) {
9292
+ setError(noteErrorSentence(e));
9293
+ }
9294
+ }, [orderId]);
9295
+ React.useEffect(() => {
9296
+ void load();
9297
+ }, [load]);
9298
+ const processing = notes?.find((n) => n.status === "processing" || n.cancellation === "processing");
9299
+ React.useEffect(() => {
9300
+ if (!processing) return;
9301
+ let alive = true;
9302
+ let step = 0;
9303
+ let timer;
9304
+ const tick = async () => {
9305
+ if (!alive || step >= POLL_DELAYS2.length) return;
9306
+ timer = setTimeout(async () => {
9307
+ step++;
9308
+ try {
9309
+ const fresh = await orderNotes.read(processing.documentId);
9310
+ if (!alive) return;
9311
+ setNotes((list) => list?.map((n) => n.documentId === fresh.documentId ? fresh : n) ?? null);
9312
+ if (fresh.status === "processing" || fresh.cancellation === "processing") void tick();
9313
+ else if (fresh.status === "authorized" && !fresh.cancellation) toast.success(`NFC-e n\xBA ${fresh.number ?? ""} autorizada`);
9314
+ else if (fresh.status === "rejected") toast.error("A SEFAZ rejeitou a NFC-e", { description: fresh.rejection?.message ?? void 0 });
9315
+ } catch {
9316
+ void tick();
9317
+ }
9318
+ }, POLL_DELAYS2[step] * 1e3);
9319
+ };
9320
+ void tick();
9321
+ return () => {
9322
+ alive = false;
9323
+ clearTimeout(timer);
9324
+ };
9325
+ }, [processing?.documentId, processing?.status, processing?.cancellation]);
9326
+ if (!orderId) return null;
9327
+ const live = notes?.find((n) => n.status === "processing" || n.status === "authorized");
9328
+ const issue = async () => {
9329
+ setIssuing(true);
9330
+ try {
9331
+ const note = await orderNotes.issueNfce(orderId);
9332
+ setNotes((list) => [note, ...list ?? []]);
9333
+ if (note.status === "rejected") toast.error("A NFC-e foi recusada", { description: note.rejection?.message ?? void 0 });
9334
+ else toast.success("NFC-e enviada. Aguardando a SEFAZ\u2026");
9335
+ } catch (e) {
9336
+ toast.error("N\xE3o foi poss\xEDvel emitir a NFC-e", { description: noteErrorSentence(e) });
9337
+ } finally {
9338
+ setIssuing(false);
9339
+ }
9340
+ };
9341
+ return /* @__PURE__ */ jsxs(
9342
+ OrderDetailBlock,
9343
+ {
9344
+ title: "Nota fiscal",
9345
+ actions: !live && notes !== null && !error ? /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => void issue(), disabled: issuing, children: [
9346
+ /* @__PURE__ */ jsx(FileText, { className: "mr-1.5 h-4 w-4" }),
9347
+ issuing ? "Emitindo\u2026" : "Emitir NFC-e"
9348
+ ] }) : void 0,
9349
+ children: [
9350
+ error && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [
9351
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: error }),
9352
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => void load(), children: /* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4" }) })
9353
+ ] }),
9354
+ !error && notes === null && /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
9355
+ !error && notes?.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhuma nota emitida para este pedido." }),
9356
+ !error && notes && notes.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-3", children: notes.map((n) => /* @__PURE__ */ jsxs("li", { className: "space-y-1.5 text-sm", children: [
9357
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
9358
+ /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
9359
+ n.model === "nfce" ? "NFC-e" : "NF-e",
9360
+ n.number ? ` n\xBA ${n.number}` : "",
9361
+ n.series ? ` \xB7 s\xE9rie ${n.series}` : ""
9362
+ ] }),
9363
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", className: tone(n.status), children: n.cancellation === "processing" ? "Cancelando\u2026" : n.uncertain ? "Aguardando a SEFAZ" : NOTE_STATUS_LABEL[n.status] }),
9364
+ n.environment === "homologation" && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: "Homologa\xE7\xE3o" })
9365
+ ] }),
9366
+ n.accessKey && /* @__PURE__ */ jsx("p", { className: "break-all font-mono text-xs text-muted-foreground", children: n.accessKey.replace(/(\d{4})(?=\d)/g, "$1 ") }),
9367
+ (n.status === "rejected" || n.status === "error") && n.rejection?.message && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: n.rejection.message }),
9368
+ n.status === "cancelled" && n.cancelReason && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
9369
+ "Cancelada: ",
9370
+ n.cancelReason
9371
+ ] }),
9372
+ (n.status === "authorized" || n.status === "cancelled") && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
9373
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => void orderNotes.pdf(n.documentId).then((b) => openFile(b, `nfce-${n.number ?? n.documentId}.pdf`, false)).catch((e) => toast.error("N\xE3o foi poss\xEDvel abrir o DANFE", { description: noteErrorSentence(e) })), children: [
9374
+ /* @__PURE__ */ jsx(FileText, { className: "mr-1.5 h-4 w-4" }),
9375
+ "DANFE"
9376
+ ] }),
9377
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => void orderNotes.xml(n.documentId).then((b) => openFile(b, `nfce-${n.number ?? n.documentId}.xml`, true)).catch((e) => toast.error("N\xE3o foi poss\xEDvel baixar o XML", { description: noteErrorSentence(e) })), children: [
9378
+ /* @__PURE__ */ jsx(Download, { className: "mr-1.5 h-4 w-4" }),
9379
+ "XML"
9380
+ ] }),
9381
+ n.status === "authorized" && !n.cancellation && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "text-destructive", onClick: () => setCancelling(n), children: "Cancelar nota" })
9382
+ ] })
9383
+ ] }, n.documentId)) }),
9384
+ cancelling && /* @__PURE__ */ jsx(
9385
+ CancelDialog,
9386
+ {
9387
+ note: cancelling,
9388
+ onClose: () => setCancelling(null),
9389
+ onDone: (fresh) => {
9390
+ setNotes((list) => list?.map((x) => x.documentId === fresh.documentId ? fresh : x) ?? null);
9391
+ setCancelling(null);
9392
+ }
9393
+ }
9394
+ )
9395
+ ]
9396
+ }
9397
+ );
9398
+ }
9399
+ function CancelDialog({ note, onClose, onDone }) {
9400
+ const [text, setText] = React.useState("");
9401
+ const [sending, setSending] = React.useState(false);
9402
+ const ok = text.trim().length >= CANCEL_MIN_JUSTIFICATION;
9403
+ const send2 = async () => {
9404
+ setSending(true);
9405
+ try {
9406
+ const fresh = await orderNotes.cancel(note.documentId, text.trim());
9407
+ toast.success("Cancelamento enviado. Aguardando a SEFAZ\u2026");
9408
+ onDone(fresh);
9409
+ } catch (e) {
9410
+ toast.error("N\xE3o foi poss\xEDvel cancelar a nota", { description: noteErrorSentence(e) });
9411
+ } finally {
9412
+ setSending(false);
9413
+ }
9414
+ };
9415
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (open) => {
9416
+ if (!open) onClose();
9417
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { children: [
9418
+ /* @__PURE__ */ jsxs(ModalHeader, { children: [
9419
+ /* @__PURE__ */ jsxs(ModalTitle, { children: [
9420
+ "Cancelar NFC-e",
9421
+ note.number ? ` n\xBA ${note.number}` : ""
9422
+ ] }),
9423
+ /* @__PURE__ */ jsx(ModalDescription, { children: "A SEFAZ s\xF3 aceita o cancelamento de uma NFC-e logo depois da emiss\xE3o (em geral, 30 minutos). Para emitir uma NF-e no lugar, cancele primeiro esta nota." })
9424
+ ] }),
9425
+ /* @__PURE__ */ jsx(
9426
+ Textarea,
9427
+ {
9428
+ value: text,
9429
+ onChange: (e) => setText(e.target.value),
9430
+ placeholder: "Ex.: Emiss\xE3o em duplicidade por erro operacional",
9431
+ rows: 3
9432
+ }
9433
+ ),
9434
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
9435
+ "Pelo menos ",
9436
+ CANCEL_MIN_JUSTIFICATION,
9437
+ " caracteres."
9438
+ ] }),
9439
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
9440
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: sending, children: "Voltar" }),
9441
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => void send2(), disabled: !ok || sending, children: sending ? "Cancelando\u2026" : "Cancelar nota" })
9442
+ ] })
9443
+ ] }) });
9444
+ }
8894
9445
 
8895
9446
  // src/lib/financial-provider.ts
8896
9447
  function fiscalReader(provider) {
@@ -9181,6 +9732,14 @@ function createPlugNotasPlugin(options = {}) {
9181
9732
  order: 0,
9182
9733
  permission: { feature: "financial", action: "read" }
9183
9734
  },
9735
+ {
9736
+ // Issue, follow and cancel the order's NFC-e (control-sdk-backend ADR 0008).
9737
+ id: "fiscal-br-order-note",
9738
+ zone: ORDER_DETAIL_ZONES.fiscal,
9739
+ component: OrderNoteCard,
9740
+ order: 0,
9741
+ permission: { feature: "financial", action: "read" }
9742
+ },
9184
9743
  {
9185
9744
  id: "fiscal-br-unit-fiscal",
9186
9745
  zone: UNIT_DETAIL_ZONES.fiscal,
@@ -9201,6 +9760,6 @@ function createPlugNotasPlugin(options = {}) {
9201
9760
  });
9202
9761
  }
9203
9762
 
9204
- export { EVENT_LABEL, EmitterError, EmittersPanel, GATE_LABELS, InboundNoteDocumentModal, InboundNotesPage, InboundNotesView, InvoiceNotesButton, IssuedNotesView, MODEL_LABEL, NfeParseError, NoteReconcileView, PLUGNOTAS_CONNECTOR_ID, PullByKeyError, ReturnedNotesView, SPECIAL_TAX_REGIMES, STATUS_META, STOCK_BEARING_ENTRY_TYPES, TAX_REGIMES, TaxGroupsView, acceptNote, addNoteAttachment, cancelNote, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, emitterErrorSentence, emitters, eventStatusLabel, explainRejection, extractPaymentCode, fetchNotePdf, fillInboxLines, findInboxNoteByNumber, findInboxNoteByOperation, getInboxIntakeHealth, getInboxNote, getInboxNotePdf, getInboxNoteXml, getNotePayable, healInboxNote, inboundNoteDocumentModal, inboundNotesInventoryView, isStockBearing, isValidCnpj, isWithoutEffect, linkInboxSupplier, listChartAccounts, listInboxNotes, listInvoiceNotes, listMeasurementUnits, listNoteAttachments, listNoteEvents, listProductStockUnits, listProductsByIds, listStockLocations, listSupplierMap, moveNoteAttachmentsToInvoice, notesButtonState, onlyDigits, parseBoleto, parseNfeXml, paymentLabel, paymentTypeFromCode, pullNoteByKey, recipientLabel, refreshNote, rejectNote, removeNoteAttachment, saveNoteDecision, searchProducts, stageNoteFromXml, statusLabel, suggestNoteItems, summarizeNotes, syncNewNotes, toStagePayload };
9763
+ export { CANCEL_MIN_JUSTIFICATION, EVENT_LABEL, EmitterError, EmittersPanel, GATE_LABELS, InboundNoteDocumentModal, InboundNotesPage, InboundNotesView, InvoiceNotesButton, IssuedNotesView, MODEL_LABEL, NOTE_STATUS_LABEL, NfeParseError, NoteReconcileView, OrderNoteCard, PLUGNOTAS_CONNECTOR_ID, PullByKeyError, ReturnedNotesView, SPECIAL_TAX_REGIMES, STATUS_META, STOCK_BEARING_ENTRY_TYPES, TAX_REGIMES, TaxGroupsView, acceptNote, addNoteAttachment, cancelNote, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, emitterErrorSentence, emitters, eventStatusLabel, explainRejection, extractPaymentCode, fetchNotePdf, fillInboxLines, findInboxNoteByNumber, findInboxNoteByOperation, getInboxIntakeHealth, getInboxNote, getInboxNotePdf, getInboxNoteXml, getNotePayable, healInboxNote, inboundNoteDocumentModal, inboundNotesInventoryView, isStockBearing, isValidCnpj, isWithoutEffect, linkInboxSupplier, listChartAccounts, listInboxNotes, listInvoiceNotes, listMeasurementUnits, listNoteAttachments, listNoteEvents, listProductStockUnits, listProductsByIds, listStockLocations, listSupplierMap, moveNoteAttachmentsToInvoice, noteErrorSentence, notesButtonState, onlyDigits, orderNotes, parseBoleto, parseNfeXml, paymentLabel, paymentTypeFromCode, pullNoteByKey, recipientLabel, refreshNote, rejectNote, removeNoteAttachment, saveNoteDecision, searchProducts, stageNoteFromXml, statusLabel, suggestNoteItems, summarizeNotes, syncNewNotes, toStagePayload };
9205
9764
  //# sourceMappingURL=index.js.map
9206
9765
  //# sourceMappingURL=index.js.map