16055cdc41
Reporter @Rekoo-PS confirmed the language fix unblocked sending, then
hit a second gap: their template uses only `{{1}} = event_name` +
`{{2}} = gallery_link`, but the legacy `buildComponents` hardcoded all
5 positional values from the `gallery_ready` shape (customer_name,
event_name, gallery_link, password_line, expiry_date). Meta rejected
with a parameter-count mismatch even after the language matched.
This adds a per-config slot list — which built-in values to send, and
in what positional order — so admins can match templates of any shape
without code changes.
## Schema (migration 138)
Additive `template_params` TEXT column on `whatsapp_configs` (default
empty string = legacy 5-slot behaviour for existing installs). Stored
as a JSON-serialized array of slot keys: `customer_name`, `event_name`,
`gallery_link`, `password_line`, `expiry_date`. Unknown / duplicate /
non-string entries are sanitized out at read time.
## Processor
- `parseTemplateParams(raw)` — defensive parser; falls back to the
5-slot default on empty / malformed / all-invalid input.
- `buildComponents(data, metaLang, params)` — emits ONLY the listed
slots in the listed order, computed via a small switch on slot key.
The password line still receives the locale-specific 🔒 label and
the empty-when-no-real-password sentinel handling.
- Processor reads `config.template_params` once per cycle and passes
the parsed array to `buildComponents` per message.
## Admin route
- GET surfaces `template_params` as the parsed array (default 5-slot
when null/empty).
- PUT round-trips the incoming array through `parseTemplateParams`
before persisting, so the stored value is always the canonical
sanitized JSON.
- Test send rebuilt to use the same `buildComponents` path so the
admin's test message matches their configured slot shape — a
reporter who configures 2 slots gets a 2-parameter test send, not
the legacy 5-parameter payload.
## UI
- `WhatsAppTab` gets a checkbox + up/down list under the Template
language field. Each slot shows its current `{{N}}` position when
checked, an em-dash when unchecked. Live preview below the list:
"Your template will receive: {{1}} = event_name, {{2}} = gallery_link".
- EN + DE i18n for the field labels, hint, preview, and per-slot
human-readable names.
## Tests
- 17 unit tests in `__tests__/utils/whatsappBuildComponents.test.js`
covering: parseTemplateParams sanitization (unknown keys, duplicates,
non-strings, malformed JSON, all-invalid fallback, pre-parsed array
acceptance) and buildComponents shape (reporter's 2-slot case,
reorder, empty list, locale-specific password label, password
sentinel handling, expiry omission).
- All 17 + the 34 existing networkValidation tests pass.
## Migration numbering
Sits at 138 on top of PR #649's migration 137. If #646 (Live Slideshow)
merges before this, #646's own 137 + 138 take precedence and this
needs renumbering to 139. Coordinated via PR #646's review thread.
## Honest caveat
Still no Meta Business API account on my side. Spec-built, sanitizer +
shape unit-tested, lint + tsc clean. End-to-end against Meta needs the
reporter (or a maintainer with an account) to verify. If a real
round-trip surfaces a mismatch, drop it in #647 and I'll iterate.
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { api } from '../config/api';
|
|
|
|
/**
|
|
* WhatsApp Business API admin config (#640D). The access token is masked on
|
|
* GET — the server returns `'********'` when a token is stored, the empty
|
|
* string when none is. The PUT silently preserves the stored token if the
|
|
* masked sentinel is sent back unchanged.
|
|
*/
|
|
// Slot keys that map to the built-in `message_data` fields the queue
|
|
// processor knows how to substitute. Order = positional `{{N}}` order in the
|
|
// Meta-registered template body. Any other string is dropped server-side.
|
|
export type WhatsAppTemplateParam =
|
|
| 'customer_name'
|
|
| 'event_name'
|
|
| 'gallery_link'
|
|
| 'password_line'
|
|
| 'expiry_date';
|
|
|
|
export const WHATSAPP_TEMPLATE_PARAMS: WhatsAppTemplateParam[] = [
|
|
'customer_name',
|
|
'event_name',
|
|
'gallery_link',
|
|
'password_line',
|
|
'expiry_date',
|
|
];
|
|
|
|
export interface WhatsAppConfig {
|
|
phone_number_id: string;
|
|
waba_id: string;
|
|
access_token: string; // masked '********' on GET when a real token is stored
|
|
template_name: string;
|
|
// Meta template language code (`ar`, `en_US`, `de_DE`, etc.) (#647). Must
|
|
// match the language the operator registered with Meta for `template_name`,
|
|
// otherwise Meta returns template_not_found_in_language (132001). Empty
|
|
// string falls through to general_default_language.
|
|
template_language: string;
|
|
// Ordered slot list controlling which built-in values are sent as
|
|
// positional `{{N}}` parameters to Meta, and in what order (#647
|
|
// follow-up). Empty (server-side) falls back to the legacy 5-slot
|
|
// gallery_ready shape so existing installs keep working unchanged.
|
|
template_params: WhatsAppTemplateParam[];
|
|
enabled: boolean;
|
|
}
|
|
|
|
export const whatsappService = {
|
|
async getConfig(): Promise<WhatsAppConfig> {
|
|
const response = await api.get<WhatsAppConfig>('/admin/whatsapp/config');
|
|
return response.data;
|
|
},
|
|
|
|
async updateConfig(config: Partial<WhatsAppConfig>): Promise<{ success: true }> {
|
|
const response = await api.put<{ success: true }>('/admin/whatsapp/config', config);
|
|
return response.data;
|
|
},
|
|
|
|
// Sends a static test message to the supplied phone number using the
|
|
// currently-saved config. Returns the Meta message ID on success.
|
|
async sendTest(phone: string): Promise<{ success: boolean; messageId?: string }> {
|
|
const response = await api.post<{ success: boolean; messageId?: string }>(
|
|
'/admin/whatsapp/test',
|
|
{ phone },
|
|
);
|
|
return response.data;
|
|
},
|
|
};
|