@neetru/sdk 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/ai.d.cts +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/auth.cjs +179 -2
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +179 -2
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +17 -0
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +17 -1
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/index.cjs +180 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +180 -3
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +68 -1
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +17 -1
- package/dist/mocks.d.ts +17 -1
- package/dist/mocks.mjs +68 -1
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/support.cjs +98 -1
- package/dist/support.cjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/support.mjs +98 -1
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/{types-DQplLXIk.d.cts → types-BoFSCHF_.d.cts} +117 -2
- package/dist/{types-DslphKIM.d.ts → types-DdJyqBy-.d.ts} +117 -2
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to `@neetru/sdk` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.4.0] - 2026-08-15 — **feat(support): superfície "Product Feedback" — markResolvedSeen/recordSatisfactionAttempt/submitSatisfaction/contestTicket**
|
|
9
|
+
|
|
10
|
+
Release **minor** (API pública aditiva no namespace `client.support`: 4 métodos novos + 9 campos novos em `SupportTicket` + 3 opcionais em `CreateTicketInput`).
|
|
11
|
+
|
|
12
|
+
### Contexto
|
|
13
|
+
|
|
14
|
+
Camada de dados do fluxo "Product Feedback" (report → resolvido → satisfação → contestação), contrato compartilhado entre Core (endpoints+schema), SDK (`@neetru/sdk` namespace `support`) e `@neetru/product-feedback` (neetru-libs). Ver `CONTRATO_PRODUCT_FEEDBACK_2026-08-15.md`. Decisões do owner: rating CSAT 1-5 (`number | null`); contestação incluída no v1 (4 estados).
|
|
15
|
+
|
|
16
|
+
### Adicionado
|
|
17
|
+
|
|
18
|
+
- **`createTicket(input)`**: aceita opcionais `pageUrl?: string`, `screenshotBase64?: string`, `consoleLogs?: string` — propagados no body do POST quando presentes.
|
|
19
|
+
- **`SupportTicket`**: campos novos (todos nullable, mapeados defensivamente em `toTicket()`) — `pageUrl`, `screenshotUrl` (= `attachments[0] ?? null`), `resolvedAt`, `resolvedNotifiedAt`, `satisfactionAttempts` (default `0`), `lastSatisfactionAttemptAt`, `satisfiedAt`, `rating`, `satisfactionComment`.
|
|
20
|
+
- **`markResolvedSeen(ticketId)`**: `POST /api/sdk/v1/support/tickets/{id}/resolved-seen` — marca a notificação de resolução como vista (idempotente no Core).
|
|
21
|
+
- **`recordSatisfactionAttempt(ticketId)`**: `POST .../satisfaction-attempt` — registra exibição do modal de satisfação (regra-chave do fluxo: exibir já conta como tentativa, evita reprompt infinito).
|
|
22
|
+
- **`submitSatisfaction({ ticketId, rating, comment?, skipped })`**: `POST .../satisfaction` — envia (ou pula) a avaliação CSAT.
|
|
23
|
+
- **`contestTicket(ticketId, { comment? })`**: `POST .../contest` — reabre um ticket resolvido e zera o ciclo de satisfação/notificação.
|
|
24
|
+
- Validação client-side antes de cada request: `ticketId` non-empty (`validation_failed`); em `submitSatisfaction`, `rating` inteiro 1-5 quando não-`null`; `comment` ≤ 2000 chars.
|
|
25
|
+
- `MockSupport` espelha os 4 métodos novos in-memory (FSM coerente com o contrato) — modo dev/test sem fetch real.
|
|
26
|
+
- Testes novos cobrindo cada método (path, `requireAuth`, validação client-side), propagação dos campos novos em `createTicket`, e mapeamento defensivo em `toTicket()`.
|
|
27
|
+
|
|
28
|
+
### Corrigido (incluído neste minor — `bug_58811a43`)
|
|
29
|
+
|
|
30
|
+
- **JSDoc de `NeetruDbOptions.dbId`** (`src/db/client-db.ts`): reescrito pra descrever o comportamento real (nomeia o cache offline LOCAL + filtro de subscription `nosql-vm`); explicita o que NÃO faz (não escopa as escritas REST — o escopo é `x-neetru-tenant`; não é usado pelo lease SQL, que usa `SqlOptions.database`). O texto antigo ("necessário para scoping das coleções") enganava.
|
|
31
|
+
- **Aviso dev-only one-time** `warnMissingDbIdOnce` (`createNeetruDb`): `console.warn` único em contexto browser + fora de `prod` quando `dbId` está ausente. **Sem mudança de comportamento de wire.**
|
|
32
|
+
|
|
8
33
|
## [3.3.0] - 2026-08-09 — **feat(db-sql): motor de ambientes "suspenso" — `db_suspended` não-retryável + lease inicial LAZY**
|
|
9
34
|
|
|
10
35
|
Release **minor** (API pública aditiva: novo `NeetruDbErrorCode` `db_suspended` + `NeetruError.reason`; `client.db.sql()` deixa de explodir o import quando o ambiente está fora no boot).
|
package/dist/ai.d.cts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart,
|
|
1
|
+
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, aI as createAiNamespace } from './types-BoFSCHF_.cjs';
|
|
2
2
|
import 'drizzle-orm/node-postgres';
|
|
3
3
|
import './errors.cjs';
|
package/dist/ai.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart,
|
|
1
|
+
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, aI as createAiNamespace } from './types-DdJyqBy-.js';
|
|
2
2
|
import 'drizzle-orm/node-postgres';
|
|
3
3
|
import './errors.js';
|
package/dist/auth.cjs
CHANGED
|
@@ -1584,6 +1584,7 @@ function toTicket(raw) {
|
|
|
1584
1584
|
if (typeof r.id !== "string") {
|
|
1585
1585
|
throw new NeetruError("invalid_response", "Ticket missing id");
|
|
1586
1586
|
}
|
|
1587
|
+
const firstAttachment = Array.isArray(r.attachments) && typeof r.attachments[0] === "string" ? r.attachments[0] : void 0;
|
|
1587
1588
|
return {
|
|
1588
1589
|
id: r.id,
|
|
1589
1590
|
subject: typeof r.subject === "string" ? r.subject : "",
|
|
@@ -1592,9 +1593,28 @@ function toTicket(raw) {
|
|
|
1592
1593
|
status: VALID_STATUSES.includes(r.status) ? r.status : "open",
|
|
1593
1594
|
createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1594
1595
|
updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
|
|
1595
|
-
productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0
|
|
1596
|
+
productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0,
|
|
1597
|
+
pageUrl: typeof r.pageUrl === "string" ? r.pageUrl : null,
|
|
1598
|
+
screenshotUrl: typeof r.screenshotUrl === "string" ? r.screenshotUrl : firstAttachment ?? null,
|
|
1599
|
+
resolvedAt: typeof r.resolvedAt === "string" ? r.resolvedAt : null,
|
|
1600
|
+
resolvedNotifiedAt: typeof r.resolvedNotifiedAt === "string" ? r.resolvedNotifiedAt : null,
|
|
1601
|
+
satisfactionAttempts: typeof r.satisfactionAttempts === "number" && Number.isFinite(r.satisfactionAttempts) ? r.satisfactionAttempts : 0,
|
|
1602
|
+
lastSatisfactionAttemptAt: typeof r.lastSatisfactionAttemptAt === "string" ? r.lastSatisfactionAttemptAt : null,
|
|
1603
|
+
satisfiedAt: typeof r.satisfiedAt === "string" ? r.satisfiedAt : null,
|
|
1604
|
+
rating: typeof r.rating === "number" && Number.isFinite(r.rating) ? r.rating : null,
|
|
1605
|
+
satisfactionComment: typeof r.satisfactionComment === "string" ? r.satisfactionComment : null
|
|
1596
1606
|
};
|
|
1597
1607
|
}
|
|
1608
|
+
function assertTicketId(ticketId) {
|
|
1609
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
1610
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function assertValidComment(comment) {
|
|
1614
|
+
if (typeof comment === "string" && comment.length > 2e3) {
|
|
1615
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1598
1618
|
function resolveProductId(config, ...candidates) {
|
|
1599
1619
|
for (const c of candidates) {
|
|
1600
1620
|
if (typeof c === "string" && c.length > 0) {
|
|
@@ -1651,6 +1671,15 @@ function createSupportNamespace(config) {
|
|
|
1651
1671
|
if (input.metadata && typeof input.metadata === "object") {
|
|
1652
1672
|
body.metadata = input.metadata;
|
|
1653
1673
|
}
|
|
1674
|
+
if (typeof input.pageUrl === "string") {
|
|
1675
|
+
body.pageUrl = input.pageUrl;
|
|
1676
|
+
}
|
|
1677
|
+
if (typeof input.screenshotBase64 === "string") {
|
|
1678
|
+
body.screenshotBase64 = input.screenshotBase64;
|
|
1679
|
+
}
|
|
1680
|
+
if (typeof input.consoleLogs === "string") {
|
|
1681
|
+
body.consoleLogs = input.consoleLogs;
|
|
1682
|
+
}
|
|
1654
1683
|
const raw = await httpRequest(config, {
|
|
1655
1684
|
method: "POST",
|
|
1656
1685
|
path: "/api/sdk/v1/support/tickets",
|
|
@@ -1676,6 +1705,74 @@ function createSupportNamespace(config) {
|
|
|
1676
1705
|
});
|
|
1677
1706
|
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
|
|
1678
1707
|
return list.map(toTicket);
|
|
1708
|
+
},
|
|
1709
|
+
/**
|
|
1710
|
+
* 3.4: marca a notificação de resolução como vista (`resolvedNotifiedAt`,
|
|
1711
|
+
* idempotente no Core). Endpoint canonical:
|
|
1712
|
+
* `POST /api/sdk/v1/support/tickets/{id}/resolved-seen`.
|
|
1713
|
+
*/
|
|
1714
|
+
async markResolvedSeen(ticketId) {
|
|
1715
|
+
assertTicketId(ticketId);
|
|
1716
|
+
await httpRequest(config, {
|
|
1717
|
+
method: "POST",
|
|
1718
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/resolved-seen`,
|
|
1719
|
+
requireAuth: true
|
|
1720
|
+
});
|
|
1721
|
+
},
|
|
1722
|
+
/**
|
|
1723
|
+
* 3.4: registra uma tentativa de exibição do modal de satisfação
|
|
1724
|
+
* (`satisfactionAttempts += 1`). Chamar no SHOW do modal, não na
|
|
1725
|
+
* resposta. Endpoint canonical:
|
|
1726
|
+
* `POST /api/sdk/v1/support/tickets/{id}/satisfaction-attempt`.
|
|
1727
|
+
*/
|
|
1728
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
1729
|
+
assertTicketId(ticketId);
|
|
1730
|
+
await httpRequest(config, {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/satisfaction-attempt`,
|
|
1733
|
+
requireAuth: true
|
|
1734
|
+
});
|
|
1735
|
+
},
|
|
1736
|
+
/**
|
|
1737
|
+
* 3.4: envia (ou pula) a avaliação de satisfação (CSAT 1-5). Endpoint
|
|
1738
|
+
* canonical: `POST /api/sdk/v1/support/tickets/{id}/satisfaction`.
|
|
1739
|
+
*/
|
|
1740
|
+
async submitSatisfaction(input) {
|
|
1741
|
+
if (!input || typeof input !== "object") {
|
|
1742
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
1743
|
+
}
|
|
1744
|
+
assertTicketId(input.ticketId);
|
|
1745
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
1746
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
1747
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
assertValidComment(input.comment);
|
|
1751
|
+
await httpRequest(config, {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(input.ticketId)}/satisfaction`,
|
|
1754
|
+
body: {
|
|
1755
|
+
rating: input.rating ?? null,
|
|
1756
|
+
comment: input.comment ?? null,
|
|
1757
|
+
skipped: input.skipped
|
|
1758
|
+
},
|
|
1759
|
+
requireAuth: true
|
|
1760
|
+
});
|
|
1761
|
+
},
|
|
1762
|
+
/**
|
|
1763
|
+
* 3.4: contesta um ticket resolvido — reabre e zera o ciclo de
|
|
1764
|
+
* satisfação/notificação. Endpoint canonical:
|
|
1765
|
+
* `POST /api/sdk/v1/support/tickets/{id}/contest`.
|
|
1766
|
+
*/
|
|
1767
|
+
async contestTicket(ticketId, opts) {
|
|
1768
|
+
assertTicketId(ticketId);
|
|
1769
|
+
assertValidComment(opts?.comment);
|
|
1770
|
+
await httpRequest(config, {
|
|
1771
|
+
method: "POST",
|
|
1772
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/contest`,
|
|
1773
|
+
body: { comment: opts?.comment ?? void 0 },
|
|
1774
|
+
requireAuth: true
|
|
1775
|
+
});
|
|
1679
1776
|
}
|
|
1680
1777
|
};
|
|
1681
1778
|
}
|
|
@@ -5096,7 +5193,20 @@ function buildTicketProvider(config, opts) {
|
|
|
5096
5193
|
return ticket;
|
|
5097
5194
|
};
|
|
5098
5195
|
}
|
|
5196
|
+
var _hasWarnedMissingDbId = false;
|
|
5197
|
+
function warnMissingDbIdOnce(config, dbOpts) {
|
|
5198
|
+
if (_hasWarnedMissingDbId) return;
|
|
5199
|
+
if (dbOpts.dbId) return;
|
|
5200
|
+
if (config.env === "prod") return;
|
|
5201
|
+
if (typeof window === "undefined") return;
|
|
5202
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
5203
|
+
_hasWarnedMissingDbId = true;
|
|
5204
|
+
console.warn(
|
|
5205
|
+
"[neetru/sdk] client.db sem `dbId` expl\xEDcito: escritas client-side (add/set/update/remove/list) caem no datastore gen\xE9rico do Core, escopado por tenant/produto (header x-neetru-tenant) \u2014 `dbId` s\xF3 nomeia o cache offline LOCAL (IndexedDB), n\xE3o roteia nem escopa nada no servidor. Veja o JSDoc de NeetruDbOptions.dbId."
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5099
5208
|
function createNeetruDb(config, dbOpts = {}) {
|
|
5209
|
+
warnMissingDbIdOnce(config, dbOpts);
|
|
5100
5210
|
let resolvedTransport;
|
|
5101
5211
|
if (dbOpts.transport !== void 0) {
|
|
5102
5212
|
resolvedTransport = dbOpts.transport;
|
|
@@ -6519,7 +6629,18 @@ var MockSupport = class {
|
|
|
6519
6629
|
severity: input.severity ?? "normal",
|
|
6520
6630
|
status: "open",
|
|
6521
6631
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6522
|
-
productSlug: input.productSlug
|
|
6632
|
+
productSlug: input.productSlug,
|
|
6633
|
+
// 3.4 (product-feedback): espelha o contrato — campos novos nullable,
|
|
6634
|
+
// populados a partir do input quando fornecidos.
|
|
6635
|
+
pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
|
|
6636
|
+
screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
|
|
6637
|
+
resolvedAt: null,
|
|
6638
|
+
resolvedNotifiedAt: null,
|
|
6639
|
+
satisfactionAttempts: 0,
|
|
6640
|
+
lastSatisfactionAttemptAt: null,
|
|
6641
|
+
satisfiedAt: null,
|
|
6642
|
+
rating: null,
|
|
6643
|
+
satisfactionComment: null
|
|
6523
6644
|
};
|
|
6524
6645
|
this._tickets.push(ticket);
|
|
6525
6646
|
return ticket;
|
|
@@ -6530,6 +6651,62 @@ var MockSupport = class {
|
|
|
6530
6651
|
}
|
|
6531
6652
|
return [...this._tickets];
|
|
6532
6653
|
}
|
|
6654
|
+
_find(ticketId) {
|
|
6655
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
6656
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
6657
|
+
}
|
|
6658
|
+
const ticket = this._tickets.find((t) => t.id === ticketId);
|
|
6659
|
+
if (!ticket) {
|
|
6660
|
+
throw new NeetruError("not_found", `ticket ${ticketId} not found`);
|
|
6661
|
+
}
|
|
6662
|
+
return ticket;
|
|
6663
|
+
}
|
|
6664
|
+
/** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
|
|
6665
|
+
async markResolvedSeen(ticketId) {
|
|
6666
|
+
const ticket = this._find(ticketId);
|
|
6667
|
+
if (ticket.resolvedNotifiedAt === null) {
|
|
6668
|
+
ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
/** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
|
|
6672
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
6673
|
+
const ticket = this._find(ticketId);
|
|
6674
|
+
ticket.satisfactionAttempts += 1;
|
|
6675
|
+
ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6676
|
+
}
|
|
6677
|
+
/** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
|
|
6678
|
+
async submitSatisfaction(input) {
|
|
6679
|
+
if (!input || typeof input !== "object") {
|
|
6680
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
6681
|
+
}
|
|
6682
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
6683
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
6684
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
if (typeof input.comment === "string" && input.comment.length > 2e3) {
|
|
6688
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6689
|
+
}
|
|
6690
|
+
const ticket = this._find(input.ticketId);
|
|
6691
|
+
ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6692
|
+
ticket.rating = input.rating ?? null;
|
|
6693
|
+
ticket.satisfactionComment = input.comment ?? null;
|
|
6694
|
+
}
|
|
6695
|
+
/** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
|
|
6696
|
+
async contestTicket(ticketId, opts) {
|
|
6697
|
+
if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
|
|
6698
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6699
|
+
}
|
|
6700
|
+
const ticket = this._find(ticketId);
|
|
6701
|
+
ticket.status = "open";
|
|
6702
|
+
ticket.resolvedAt = null;
|
|
6703
|
+
ticket.resolvedNotifiedAt = null;
|
|
6704
|
+
ticket.satisfactionAttempts = 0;
|
|
6705
|
+
ticket.lastSatisfactionAttemptAt = null;
|
|
6706
|
+
ticket.satisfiedAt = null;
|
|
6707
|
+
ticket.rating = null;
|
|
6708
|
+
ticket.satisfactionComment = null;
|
|
6709
|
+
}
|
|
6533
6710
|
/** Test helper. */
|
|
6534
6711
|
__reset() {
|
|
6535
6712
|
this._tickets = [];
|