optimal-cli 0.1.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/README.md +175 -0
- package/dist/bin/optimal.d.ts +2 -0
- package/dist/bin/optimal.js +995 -0
- package/dist/lib/budget/projections.d.ts +115 -0
- package/dist/lib/budget/projections.js +384 -0
- package/dist/lib/budget/scenarios.d.ts +93 -0
- package/dist/lib/budget/scenarios.js +214 -0
- package/dist/lib/cms/publish-blog.d.ts +62 -0
- package/dist/lib/cms/publish-blog.js +74 -0
- package/dist/lib/cms/strapi-client.d.ts +123 -0
- package/dist/lib/cms/strapi-client.js +213 -0
- package/dist/lib/config.d.ts +55 -0
- package/dist/lib/config.js +206 -0
- package/dist/lib/infra/deploy.d.ts +29 -0
- package/dist/lib/infra/deploy.js +58 -0
- package/dist/lib/infra/migrate.d.ts +34 -0
- package/dist/lib/infra/migrate.js +103 -0
- package/dist/lib/kanban.d.ts +46 -0
- package/dist/lib/kanban.js +118 -0
- package/dist/lib/newsletter/distribute.d.ts +52 -0
- package/dist/lib/newsletter/distribute.js +193 -0
- package/dist/lib/newsletter/generate-insurance.d.ts +42 -0
- package/dist/lib/newsletter/generate-insurance.js +36 -0
- package/dist/lib/newsletter/generate.d.ts +104 -0
- package/dist/lib/newsletter/generate.js +571 -0
- package/dist/lib/returnpro/anomalies.d.ts +64 -0
- package/dist/lib/returnpro/anomalies.js +166 -0
- package/dist/lib/returnpro/audit.d.ts +32 -0
- package/dist/lib/returnpro/audit.js +147 -0
- package/dist/lib/returnpro/diagnose.d.ts +52 -0
- package/dist/lib/returnpro/diagnose.js +281 -0
- package/dist/lib/returnpro/kpis.d.ts +32 -0
- package/dist/lib/returnpro/kpis.js +192 -0
- package/dist/lib/returnpro/templates.d.ts +48 -0
- package/dist/lib/returnpro/templates.js +229 -0
- package/dist/lib/returnpro/upload-income.d.ts +25 -0
- package/dist/lib/returnpro/upload-income.js +235 -0
- package/dist/lib/returnpro/upload-netsuite.d.ts +37 -0
- package/dist/lib/returnpro/upload-netsuite.js +566 -0
- package/dist/lib/returnpro/upload-r1.d.ts +48 -0
- package/dist/lib/returnpro/upload-r1.js +398 -0
- package/dist/lib/social/post-generator.d.ts +83 -0
- package/dist/lib/social/post-generator.js +333 -0
- package/dist/lib/social/publish.d.ts +66 -0
- package/dist/lib/social/publish.js +226 -0
- package/dist/lib/social/scraper.d.ts +67 -0
- package/dist/lib/social/scraper.js +361 -0
- package/dist/lib/supabase.d.ts +4 -0
- package/dist/lib/supabase.js +20 -0
- package/dist/lib/transactions/delete-batch.d.ts +60 -0
- package/dist/lib/transactions/delete-batch.js +203 -0
- package/dist/lib/transactions/ingest.d.ts +43 -0
- package/dist/lib/transactions/ingest.js +555 -0
- package/dist/lib/transactions/stamp.d.ts +51 -0
- package/dist/lib/transactions/stamp.js +524 -0
- package/package.json +50 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Distribution Module
|
|
3
|
+
*
|
|
4
|
+
* Triggers newsletter distribution via the n8n webhook and updates
|
|
5
|
+
* Strapi delivery tracking fields accordingly.
|
|
6
|
+
*
|
|
7
|
+
* Functions:
|
|
8
|
+
* distributeNewsletter(documentId, opts?) — orchestrate distribution
|
|
9
|
+
* checkDistributionStatus(documentId) — read current delivery status
|
|
10
|
+
*/
|
|
11
|
+
import 'dotenv/config';
|
|
12
|
+
export interface DistributionResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
documentId: string;
|
|
15
|
+
channel: string;
|
|
16
|
+
webhookResponse?: unknown;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DeliveryStatus {
|
|
20
|
+
documentId: string;
|
|
21
|
+
delivery_status: string;
|
|
22
|
+
delivered_at: string | null;
|
|
23
|
+
recipients_count: number | null;
|
|
24
|
+
ghl_campaign_id: string | null;
|
|
25
|
+
delivery_errors: unknown[] | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Main orchestrator: fetch newsletter from Strapi, validate state,
|
|
29
|
+
* update delivery_status to 'sending', trigger the n8n webhook,
|
|
30
|
+
* and update tracking fields based on the result.
|
|
31
|
+
*
|
|
32
|
+
* @param documentId - Strapi documentId (UUID string) of the newsletter
|
|
33
|
+
* @param opts.channel - Distribution channel. Defaults to 'all'.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const result = await distributeNewsletter('abc123-def456')
|
|
37
|
+
* const emailOnly = await distributeNewsletter('abc123-def456', { channel: 'email' })
|
|
38
|
+
*/
|
|
39
|
+
export declare function distributeNewsletter(documentId: string, opts?: {
|
|
40
|
+
channel?: 'email' | 'all';
|
|
41
|
+
}): Promise<DistributionResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch the current delivery tracking fields for a newsletter from Strapi.
|
|
44
|
+
*
|
|
45
|
+
* @param documentId - Strapi documentId (UUID string) of the newsletter
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const status = await checkDistributionStatus('abc123-def456')
|
|
49
|
+
* console.log(status.delivery_status) // 'delivered'
|
|
50
|
+
* console.log(status.recipients_count) // 847
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkDistributionStatus(documentId: string): Promise<DeliveryStatus>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Distribution Module
|
|
3
|
+
*
|
|
4
|
+
* Triggers newsletter distribution via the n8n webhook and updates
|
|
5
|
+
* Strapi delivery tracking fields accordingly.
|
|
6
|
+
*
|
|
7
|
+
* Functions:
|
|
8
|
+
* distributeNewsletter(documentId, opts?) — orchestrate distribution
|
|
9
|
+
* checkDistributionStatus(documentId) — read current delivery status
|
|
10
|
+
*/
|
|
11
|
+
import 'dotenv/config';
|
|
12
|
+
import { strapiGet, strapiPut } from '../cms/strapi-client.js';
|
|
13
|
+
// ── Environment helpers ───────────────────────────────────────────────
|
|
14
|
+
function getWebhookUrl() {
|
|
15
|
+
const url = process.env.N8N_WEBHOOK_URL;
|
|
16
|
+
if (!url) {
|
|
17
|
+
throw new Error('Missing env var: N8N_WEBHOOK_URL\n' +
|
|
18
|
+
'Set it to your n8n base URL, e.g. https://n8n.op-hub.com\n' +
|
|
19
|
+
'The distribute module will POST to: $N8N_WEBHOOK_URL/webhook/newsletter-distribute');
|
|
20
|
+
}
|
|
21
|
+
return url.replace(/\/+$/, '');
|
|
22
|
+
}
|
|
23
|
+
// ── 1. Distribute Newsletter ──────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Main orchestrator: fetch newsletter from Strapi, validate state,
|
|
26
|
+
* update delivery_status to 'sending', trigger the n8n webhook,
|
|
27
|
+
* and update tracking fields based on the result.
|
|
28
|
+
*
|
|
29
|
+
* @param documentId - Strapi documentId (UUID string) of the newsletter
|
|
30
|
+
* @param opts.channel - Distribution channel. Defaults to 'all'.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const result = await distributeNewsletter('abc123-def456')
|
|
34
|
+
* const emailOnly = await distributeNewsletter('abc123-def456', { channel: 'email' })
|
|
35
|
+
*/
|
|
36
|
+
export async function distributeNewsletter(documentId, opts = {}) {
|
|
37
|
+
const channel = opts.channel ?? 'all';
|
|
38
|
+
// Step 1: Fetch the newsletter from Strapi
|
|
39
|
+
let newsletter;
|
|
40
|
+
try {
|
|
41
|
+
const response = await strapiGet(`/api/newsletters/${documentId}`);
|
|
42
|
+
newsletter = response.data;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
documentId,
|
|
49
|
+
channel,
|
|
50
|
+
error: `Failed to fetch newsletter from Strapi: ${msg}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Step 2: Validate published state
|
|
54
|
+
if (!newsletter.publishedAt) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
documentId,
|
|
58
|
+
channel,
|
|
59
|
+
error: 'Newsletter is not published. Publish it in Strapi before distributing.',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Step 3: Validate delivery_status — only distribute if pending or unset
|
|
63
|
+
const currentStatus = newsletter.delivery_status ?? 'pending';
|
|
64
|
+
if (currentStatus !== 'pending' && currentStatus !== undefined) {
|
|
65
|
+
if (currentStatus === 'sending' ||
|
|
66
|
+
currentStatus === 'delivered' ||
|
|
67
|
+
currentStatus === 'partial') {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
documentId,
|
|
71
|
+
channel,
|
|
72
|
+
error: `Newsletter already has delivery_status="${currentStatus}". Cannot re-distribute.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Step 4: Mark as 'sending' in Strapi
|
|
77
|
+
try {
|
|
78
|
+
await strapiPut('/api/newsletters', documentId, {
|
|
79
|
+
delivery_status: 'sending',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
documentId,
|
|
87
|
+
channel,
|
|
88
|
+
error: `Failed to update delivery_status to 'sending': ${msg}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Step 5: Trigger n8n webhook
|
|
92
|
+
const webhookUrl = getWebhookUrl();
|
|
93
|
+
const webhookEndpoint = `${webhookUrl}/webhook/newsletter-distribute`;
|
|
94
|
+
const brand = typeof newsletter.brand === 'string' ? newsletter.brand : '';
|
|
95
|
+
let webhookResponse;
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(webhookEndpoint, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ documentId, brand, channel }),
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
let body;
|
|
104
|
+
try {
|
|
105
|
+
body = await res.json();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
body = await res.text().catch(() => res.statusText);
|
|
109
|
+
}
|
|
110
|
+
// Webhook failed — update Strapi to 'failed' with error details
|
|
111
|
+
const errorDetails = [
|
|
112
|
+
{ step: 'webhook', status: res.status, body },
|
|
113
|
+
];
|
|
114
|
+
await strapiPut('/api/newsletters', documentId, {
|
|
115
|
+
delivery_status: 'failed',
|
|
116
|
+
delivery_errors: errorDetails,
|
|
117
|
+
}).catch(() => {
|
|
118
|
+
// Best-effort update — don't mask the original error
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
documentId,
|
|
123
|
+
channel,
|
|
124
|
+
error: `n8n webhook returned ${res.status}: ${JSON.stringify(body)}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
webhookResponse = await res.json();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
webhookResponse = { status: res.status };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Network/connection error
|
|
136
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
137
|
+
const errorDetails = [{ step: 'webhook', error: msg }];
|
|
138
|
+
await strapiPut('/api/newsletters', documentId, {
|
|
139
|
+
delivery_status: 'failed',
|
|
140
|
+
delivery_errors: errorDetails,
|
|
141
|
+
}).catch(() => {
|
|
142
|
+
// Best-effort
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
documentId,
|
|
147
|
+
channel,
|
|
148
|
+
error: `n8n webhook request failed: ${msg}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Step 6: Webhook succeeded — return success
|
|
152
|
+
// Note: n8n will update delivery_status to 'delivered' (or 'partial') via its
|
|
153
|
+
// own Strapi PUT once it finishes sending. We don't update it here.
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
documentId,
|
|
157
|
+
channel,
|
|
158
|
+
webhookResponse,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// ── 2. Check Distribution Status ─────────────────────────────────────
|
|
162
|
+
/**
|
|
163
|
+
* Fetch the current delivery tracking fields for a newsletter from Strapi.
|
|
164
|
+
*
|
|
165
|
+
* @param documentId - Strapi documentId (UUID string) of the newsletter
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const status = await checkDistributionStatus('abc123-def456')
|
|
169
|
+
* console.log(status.delivery_status) // 'delivered'
|
|
170
|
+
* console.log(status.recipients_count) // 847
|
|
171
|
+
*/
|
|
172
|
+
export async function checkDistributionStatus(documentId) {
|
|
173
|
+
const response = await strapiGet(`/api/newsletters/${documentId}`);
|
|
174
|
+
const data = response.data;
|
|
175
|
+
return {
|
|
176
|
+
documentId,
|
|
177
|
+
delivery_status: typeof data.delivery_status === 'string'
|
|
178
|
+
? data.delivery_status
|
|
179
|
+
: 'pending',
|
|
180
|
+
delivered_at: typeof data.delivered_at === 'string'
|
|
181
|
+
? data.delivered_at
|
|
182
|
+
: null,
|
|
183
|
+
recipients_count: typeof data.recipients_count === 'number'
|
|
184
|
+
? data.recipients_count
|
|
185
|
+
: null,
|
|
186
|
+
ghl_campaign_id: typeof data.ghl_campaign_id === 'string'
|
|
187
|
+
? data.ghl_campaign_id
|
|
188
|
+
: null,
|
|
189
|
+
delivery_errors: Array.isArray(data.delivery_errors)
|
|
190
|
+
? data.delivery_errors
|
|
191
|
+
: null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insurance Newsletter Generation — LIFEINSUR (Anchor Point Insurance Co.)
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the shared generateNewsletter() orchestrator in generate.ts.
|
|
5
|
+
* All pipeline logic (NewsAPI fetch, Groq AI, HTML build, Strapi push) lives there;
|
|
6
|
+
* BRAND_CONFIGS['LIFEINSUR'] already wires up the correct news query, sender email,
|
|
7
|
+
* brand colors (charcoal #44403E / terracotta #AD7C59 / beige #FCF9F6), and
|
|
8
|
+
* hasProperties=false so no Excel step runs.
|
|
9
|
+
*
|
|
10
|
+
* Ported from: ~/projects/newsletter-automation/generate-newsletter-lifeinsur.py
|
|
11
|
+
*/
|
|
12
|
+
import { type GenerateResult } from './generate.js';
|
|
13
|
+
/** Options accepted by generateInsuranceNewsletter() */
|
|
14
|
+
export interface InsuranceNewsletterOptions {
|
|
15
|
+
/** ISO date string (YYYY-MM-DD) for the edition date; defaults to today */
|
|
16
|
+
date?: string;
|
|
17
|
+
/** When true, skips the Strapi push and returns the generated payload only */
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Result returned by generateInsuranceNewsletter().
|
|
22
|
+
* Aliased from GenerateResult for a consistent public API surface;
|
|
23
|
+
* all fields are identical — import GenerateResult from generate.ts if you
|
|
24
|
+
* need the underlying type directly.
|
|
25
|
+
*/
|
|
26
|
+
export type NewsletterResult = GenerateResult;
|
|
27
|
+
/**
|
|
28
|
+
* Generate a LIFEINSUR newsletter for Anchor Point Insurance Co.
|
|
29
|
+
*
|
|
30
|
+
* Pipeline steps (delegated to generateNewsletter):
|
|
31
|
+
* 1. Skip property extraction (LIFEINSUR has no listings)
|
|
32
|
+
* 2. Fetch life insurance news via NewsAPI
|
|
33
|
+
* (query: "life insurance coverage policy florida texas alabama")
|
|
34
|
+
* 3. Generate market overview + news summaries via Groq (Llama 3.3 70B)
|
|
35
|
+
* 4. Build branded HTML email with LIFEINSUR color scheme
|
|
36
|
+
* 5. Push draft to Strapi as brand=LIFEINSUR (unless dryRun=true)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const result = await generateInsuranceNewsletter({ dryRun: false })
|
|
40
|
+
* console.log(result.strapiDocumentId)
|
|
41
|
+
*/
|
|
42
|
+
export declare function generateInsuranceNewsletter(options?: InsuranceNewsletterOptions): Promise<NewsletterResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insurance Newsletter Generation — LIFEINSUR (Anchor Point Insurance Co.)
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the shared generateNewsletter() orchestrator in generate.ts.
|
|
5
|
+
* All pipeline logic (NewsAPI fetch, Groq AI, HTML build, Strapi push) lives there;
|
|
6
|
+
* BRAND_CONFIGS['LIFEINSUR'] already wires up the correct news query, sender email,
|
|
7
|
+
* brand colors (charcoal #44403E / terracotta #AD7C59 / beige #FCF9F6), and
|
|
8
|
+
* hasProperties=false so no Excel step runs.
|
|
9
|
+
*
|
|
10
|
+
* Ported from: ~/projects/newsletter-automation/generate-newsletter-lifeinsur.py
|
|
11
|
+
*/
|
|
12
|
+
import { generateNewsletter } from './generate.js';
|
|
13
|
+
// ── Orchestrator ──────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Generate a LIFEINSUR newsletter for Anchor Point Insurance Co.
|
|
16
|
+
*
|
|
17
|
+
* Pipeline steps (delegated to generateNewsletter):
|
|
18
|
+
* 1. Skip property extraction (LIFEINSUR has no listings)
|
|
19
|
+
* 2. Fetch life insurance news via NewsAPI
|
|
20
|
+
* (query: "life insurance coverage policy florida texas alabama")
|
|
21
|
+
* 3. Generate market overview + news summaries via Groq (Llama 3.3 70B)
|
|
22
|
+
* 4. Build branded HTML email with LIFEINSUR color scheme
|
|
23
|
+
* 5. Push draft to Strapi as brand=LIFEINSUR (unless dryRun=true)
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const result = await generateInsuranceNewsletter({ dryRun: false })
|
|
27
|
+
* console.log(result.strapiDocumentId)
|
|
28
|
+
*/
|
|
29
|
+
export async function generateInsuranceNewsletter(options = {}) {
|
|
30
|
+
return generateNewsletter({
|
|
31
|
+
brand: 'LIFEINSUR',
|
|
32
|
+
date: options.date,
|
|
33
|
+
dryRun: options.dryRun,
|
|
34
|
+
// No excelPath — LIFEINSUR has no property listings
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Generation Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Ported from Python: ~/projects/newsletter-automation/generate-newsletter-v2.py
|
|
5
|
+
*
|
|
6
|
+
* Pipeline: Excel properties -> NewsAPI -> Groq AI -> HTML build -> Strapi push
|
|
7
|
+
*
|
|
8
|
+
* Functions:
|
|
9
|
+
* fetchNews() — fetch articles from NewsAPI
|
|
10
|
+
* generateAiContent() — call Groq (OpenAI-compatible) for AI summaries
|
|
11
|
+
* readExcelProperties() — parse columnar Excel (col A = labels, B-N = properties)
|
|
12
|
+
* buildPropertyCardHtml() — render a single property card
|
|
13
|
+
* buildNewsItemHtml() — render a single news item
|
|
14
|
+
* buildHtml() — assemble full newsletter HTML
|
|
15
|
+
* buildStrapiPayload() — build the structured Strapi newsletter payload
|
|
16
|
+
* generateNewsletter() — orchestrator that runs the full pipeline
|
|
17
|
+
*/
|
|
18
|
+
export interface Property {
|
|
19
|
+
name?: string;
|
|
20
|
+
address?: string;
|
|
21
|
+
price?: string | number;
|
|
22
|
+
propertyType?: string;
|
|
23
|
+
size?: string | number;
|
|
24
|
+
region?: string;
|
|
25
|
+
highlights?: string[] | string;
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
listingUrl?: string;
|
|
28
|
+
contact?: string;
|
|
29
|
+
notes?: string;
|
|
30
|
+
analysis?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface NewsArticle {
|
|
34
|
+
title: string;
|
|
35
|
+
source: string;
|
|
36
|
+
date: string;
|
|
37
|
+
description: string;
|
|
38
|
+
url: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AiContent {
|
|
41
|
+
market_overview: string;
|
|
42
|
+
property_analyses: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
analysis: string;
|
|
45
|
+
}>;
|
|
46
|
+
news_summaries: Array<{
|
|
47
|
+
title: string;
|
|
48
|
+
analysis: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
export interface NewsletterPayload {
|
|
52
|
+
data: {
|
|
53
|
+
title: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
brand: string;
|
|
56
|
+
edition_date: string;
|
|
57
|
+
subject_line: string;
|
|
58
|
+
market_overview: string;
|
|
59
|
+
featured_properties: Property[];
|
|
60
|
+
news_items: Array<NewsArticle & {
|
|
61
|
+
analysis?: string;
|
|
62
|
+
}>;
|
|
63
|
+
html_body: string;
|
|
64
|
+
sender_email: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface BrandConfig {
|
|
68
|
+
brand: string;
|
|
69
|
+
displayName: string;
|
|
70
|
+
newsQuery: string;
|
|
71
|
+
senderEmail: string;
|
|
72
|
+
contactEmail: string;
|
|
73
|
+
titlePrefix: string;
|
|
74
|
+
subjectPrefix: string;
|
|
75
|
+
/** Whether this brand uses Excel property listings */
|
|
76
|
+
hasProperties: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface GenerateOptions {
|
|
79
|
+
brand: string;
|
|
80
|
+
date?: string;
|
|
81
|
+
excelPath?: string;
|
|
82
|
+
dryRun?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface GenerateResult {
|
|
85
|
+
properties: Property[];
|
|
86
|
+
newsArticles: NewsArticle[];
|
|
87
|
+
aiContent: AiContent | null;
|
|
88
|
+
html: string;
|
|
89
|
+
payload: NewsletterPayload;
|
|
90
|
+
strapiDocumentId: string | null;
|
|
91
|
+
}
|
|
92
|
+
export declare function getBrandConfig(brand: string): BrandConfig;
|
|
93
|
+
export declare function fetchNews(query?: string): Promise<NewsArticle[]>;
|
|
94
|
+
export declare function generateAiContent(properties: Property[], newsArticles: NewsArticle[]): Promise<AiContent | null>;
|
|
95
|
+
/**
|
|
96
|
+
* Read multiple properties from columnar Excel format.
|
|
97
|
+
* Column A = field labels, Columns B-N = one property per column.
|
|
98
|
+
*
|
|
99
|
+
* Uses exceljs (dynamically imported to keep the dependency optional).
|
|
100
|
+
*/
|
|
101
|
+
export declare function readExcelProperties(filePath: string): Promise<Property[]>;
|
|
102
|
+
export declare function buildHtml(properties: Property[], newsArticles: NewsArticle[], aiContent: AiContent | null, brandConfig: BrandConfig): string;
|
|
103
|
+
export declare function buildStrapiPayload(properties: Property[], newsArticles: NewsArticle[], aiContent: AiContent | null, html: string, brandConfig: BrandConfig, editionDate?: string): NewsletterPayload;
|
|
104
|
+
export declare function generateNewsletter(opts: GenerateOptions): Promise<GenerateResult>;
|