|
|
|
@@ -1,12 +1,14 @@
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
|
import { detectKeyword } from "../consent.js";
|
|
|
|
|
|
|
|
|
|
const mockDb = {
|
|
|
|
|
insert: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
select: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
vi.mock("@groombook/db", () => ({
|
|
|
|
|
db: {
|
|
|
|
|
insert: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
select: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
getDb: () => mockDb,
|
|
|
|
|
clients: {},
|
|
|
|
|
messageConsentEvents: {},
|
|
|
|
|
businessSettings: {},
|
|
|
|
@@ -14,7 +16,6 @@ vi.mock("@groombook/db", () => ({
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { handleConsentKeyword } = await import("../consent.js");
|
|
|
|
|
const { db } = await import("@groombook/db");
|
|
|
|
|
|
|
|
|
|
describe("detectKeyword", () => {
|
|
|
|
|
it.each([
|
|
|
|
@@ -65,10 +66,10 @@ describe("detectKeyword", () => {
|
|
|
|
|
describe("handleConsentKeyword", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
db.insert.mockReturnValue({
|
|
|
|
|
mockDb.insert.mockReturnValue({
|
|
|
|
|
values: vi.fn().mockResolvedValue([{ id: "event-1" }]),
|
|
|
|
|
} as any);
|
|
|
|
|
db.update.mockReturnValue({
|
|
|
|
|
mockDb.update.mockReturnValue({
|
|
|
|
|
set: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockResolvedValue([]),
|
|
|
|
|
}),
|
|
|
|
@@ -78,12 +79,12 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
const baseOpts = {
|
|
|
|
|
clientId: "client-1",
|
|
|
|
|
businessId: "biz-1",
|
|
|
|
|
db: db as unknown as typeof import("@groombook/db").db,
|
|
|
|
|
db: mockDb as unknown as ReturnType<typeof import("@groombook/db").getDb>,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe("opt_out", () => {
|
|
|
|
|
it("inserts consent event with sms_keyword source", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -93,11 +94,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_out" });
|
|
|
|
|
|
|
|
|
|
expect(db.insert).toHaveBeenCalledOnce();
|
|
|
|
|
expect(mockDb.insert).toHaveBeenCalledOnce();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("sets smsOptIn=false and smsOptOutDate when currently opted in", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -107,11 +108,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_out" });
|
|
|
|
|
|
|
|
|
|
expect(db.update).toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.update).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is idempotent — second opt-out logs event but skips client update", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false }]),
|
|
|
|
@@ -121,11 +122,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_out" });
|
|
|
|
|
|
|
|
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.update).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns unsubscribe reply text", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -142,7 +143,7 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
describe("opt_in", () => {
|
|
|
|
|
it("sets smsOptIn=true and smsConsentDate when currently opted out", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false, smsConsentDate: null }]),
|
|
|
|
@@ -152,11 +153,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_in" });
|
|
|
|
|
|
|
|
|
|
expect(db.update).toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.update).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("clears smsOptOutDate on opt-in after opt-out", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false }]),
|
|
|
|
@@ -166,11 +167,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_in" });
|
|
|
|
|
|
|
|
|
|
expect(db.update).toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.update).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is idempotent — second opt-in skips client update", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -180,11 +181,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_in" });
|
|
|
|
|
|
|
|
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.update).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns resubscribe reply text", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false }]),
|
|
|
|
@@ -200,34 +201,14 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("help", () => {
|
|
|
|
|
it("does not call update — opt-in state unchanged", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ messagingHelpReply: null }]),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
it("returns default help reply without querying businessSettings", async () => {
|
|
|
|
|
const result = await handleConsentKeyword({ ...baseOpts, kind: "help" });
|
|
|
|
|
|
|
|
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.select).not.toHaveBeenCalled();
|
|
|
|
|
expect(result.replyText).toBe(
|
|
|
|
|
"Reply STOP to unsubscribe or START to resubscribe. For help, contact your groomer directly."
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("uses business messagingHelpReply when configured", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ messagingHelpReply: "Custom help text." }]),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
const result = await handleConsentKeyword({ ...baseOpts, kind: "help" });
|
|
|
|
|
expect(result.replyText).toBe("Custom help text.");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|