mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado 'Quero fazer parte do time' gravado como subscription.campaignLevel (time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll suave). Contact + Subscription em withPayloadTransaction com consentimento novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o texto aprovado no admin. Migration 20260819_192605 (state de contact nullable + coluna campaign_level). Impl plan, changelog, int + e2e.
158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
// @vitest-environment node
|
|
|
|
/**
|
|
* S9 — the public campaign "novidades" capture resolves its Consent by the
|
|
* stable key `campanha-novidades` and FAILS CLOSED while the keyed document is
|
|
* missing; the engagement-level toggle choice survives as
|
|
* `subscription.campaignLevel` and a contact may be recorded without a state.
|
|
*/
|
|
import type { Payload } from 'payload'
|
|
import { getPayload } from 'payload'
|
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { submitCampaignNewsletter } from '@/app/(frontend)/actions/submitCampaignNewsletter'
|
|
import { CAMPAIGN_NEWSLETTER_CONSENT_KEY } from '@/lib/campaignConsentKeys'
|
|
import config from '@/payload.config'
|
|
|
|
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
|
|
|
let payload: Payload
|
|
const campaignFixtures = installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
const deleteNewsletterConsentRows = async () => {
|
|
// Deleting a consent fails while a subscription references it
|
|
// (`subscription.consent_id` is NOT NULL with an `ON DELETE SET NULL` FK),
|
|
// so purge referencing subscriptions first — also covers crash leftovers.
|
|
const consents = await payload.find({
|
|
collection: 'consent',
|
|
where: { key: { equals: CAMPAIGN_NEWSLETTER_CONSENT_KEY } },
|
|
depth: 0,
|
|
limit: 100,
|
|
pagination: false,
|
|
overrideAccess: true,
|
|
})
|
|
for (const consent of consents.docs) {
|
|
await payload.delete({
|
|
collection: 'subscription',
|
|
where: { consent: { equals: consent.id } },
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
})
|
|
await payload.delete({
|
|
collection: 'consent',
|
|
id: consent.id,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
const findSubscriptionByPhone = async (phone: string) => {
|
|
const contacts = await payload.find({
|
|
collection: 'contact',
|
|
where: { 'phones.value': { equals: phone } },
|
|
depth: 0,
|
|
limit: 1,
|
|
pagination: false,
|
|
overrideAccess: true,
|
|
})
|
|
const contact = contacts.docs[0]
|
|
if (!contact) return undefined
|
|
|
|
const subscriptions = await payload.find({
|
|
collection: 'subscription',
|
|
where: { contact: { equals: contact.id } },
|
|
depth: 0,
|
|
limit: 1,
|
|
pagination: false,
|
|
overrideAccess: true,
|
|
})
|
|
return { contact, subscription: subscriptions.docs[0] }
|
|
}
|
|
|
|
describe('submitCampaignNewsletter (consent by stable key + level choice)', () => {
|
|
beforeAll(async () => {
|
|
payload = await getPayload({ config: await config })
|
|
})
|
|
|
|
// One row per key is what production assumes; deleting before each test
|
|
// keeps the "most recent row wins" resolver deterministic without relying on
|
|
// created-at precision.
|
|
beforeEach(deleteNewsletterConsentRows)
|
|
|
|
it('fails closed while the keyed Consent document is missing', async () => {
|
|
await deleteNewsletterConsentRows()
|
|
|
|
await expect(
|
|
submitCampaignNewsletter({
|
|
name: campaignFixtures().personName('Contato'),
|
|
phone: campaignFixtures().phone(),
|
|
}),
|
|
).rejects.toThrow('Consentimento de novidades da campanha ainda não configurado.')
|
|
})
|
|
|
|
it('records contact + subscription with the default "time" level', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const consent = await fixtures.createConsent({ key: CAMPAIGN_NEWSLETTER_CONSENT_KEY })
|
|
const phone = fixtures.phone()
|
|
|
|
const result = await submitCampaignNewsletter({
|
|
name: fixtures.personName('Contato'),
|
|
phone,
|
|
comment: 'Quero acompanhar a campanha.',
|
|
})
|
|
expect(result).toEqual({ ok: true })
|
|
|
|
const { contact, subscription } = (await findSubscriptionByPhone(phone)) ?? {}
|
|
expect(contact).toBeDefined()
|
|
expect(subscription).toBeDefined()
|
|
// depth 0 returns the raw relationship id — the consent link itself.
|
|
expect(subscription!.consent).toBe(consent.id)
|
|
// The toggle is pre-selected: the default level is recorded as data.
|
|
expect(subscription!.campaignLevel).toBe('time')
|
|
|
|
// The action's rows are not fixture-owned — clean them here so the
|
|
// fixture's contact cleanup does not trip on the subscription FK.
|
|
await payload.delete({
|
|
collection: 'subscription',
|
|
id: subscription!.id,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
})
|
|
})
|
|
|
|
it('records the "esporadico" level and an optional state is not persisted', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const consent = await fixtures.createConsent({ key: CAMPAIGN_NEWSLETTER_CONSENT_KEY })
|
|
const phone = fixtures.phone()
|
|
|
|
const result = await submitCampaignNewsletter({
|
|
name: fixtures.personName('Contato'),
|
|
phone,
|
|
email: `${fixtures.value('nl')}@example.com`,
|
|
city: 'Salvador',
|
|
campaignLevel: 'esporadico',
|
|
})
|
|
expect(result).toEqual({ ok: true })
|
|
|
|
const { contact, subscription } = (await findSubscriptionByPhone(phone)) ?? {}
|
|
expect(contact).toBeDefined()
|
|
expect(contact!.state).toBeNull()
|
|
expect(contact!.city).toBe('Salvador')
|
|
expect(subscription).toBeDefined()
|
|
expect(subscription!.campaignLevel).toBe('esporadico')
|
|
expect(subscription!.consent).toBe(consent.id)
|
|
|
|
await payload.delete({
|
|
collection: 'subscription',
|
|
id: subscription!.id,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
})
|
|
})
|
|
})
|