docs: add comprehensive roadmap for email service improvements
This commit is contained in:
+312
@@ -0,0 +1,312 @@
|
|||||||
|
# Core-Extensions Email Service - Roadmap
|
||||||
|
|
||||||
|
## Current Status ✅
|
||||||
|
|
||||||
|
- **Order Created**: Working perfectly (customer name, phone, all languages)
|
||||||
|
- **Order Fulfilled**: Working but shows email instead of customer name
|
||||||
|
- **Order Cancelled**: Working but shows email instead of customer name
|
||||||
|
- **All webhooks firing**: ✅ Emails sent via Resend
|
||||||
|
- **Multi-language support**: ✅ Working (tested all 4 languages - SR, EN, DE, FR)
|
||||||
|
- **Admin notifications**: ✅ Both admins receiving emails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 1: Fix Broken Things (Priority)
|
||||||
|
|
||||||
|
### 1.1 Fix Customer Name in Fulfilled/Cancelled Emails
|
||||||
|
|
||||||
|
**Problem**: Fulfilled and Cancelled webhooks receive partial data from Saleor (no `shippingAddress.firstName`, no `languageCode`)
|
||||||
|
|
||||||
|
**Solution**: Query Saleor API when these webhooks fire to get full order details
|
||||||
|
|
||||||
|
**Files to modify**:
|
||||||
|
- `src/pages/api/webhooks/order-fulfilled.ts`
|
||||||
|
- `src/pages/api/webhooks/order-cancelled.ts`
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Add GraphQL query to fetch full order data including `shippingAddress` and `languageCode`
|
||||||
|
- [ ] Update email template functions to use complete order data
|
||||||
|
- [ ] Test with real fulfillments and cancellations
|
||||||
|
|
||||||
|
**Effort**: Medium
|
||||||
|
**Impact**: High (fixes broken UX)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 Fix Environment Variables Usage
|
||||||
|
|
||||||
|
**Problem**: Hardcoded values in webhook files override env vars
|
||||||
|
|
||||||
|
**Current**:
|
||||||
|
```typescript
|
||||||
|
const ADMIN_EMAILS = ["me@hytham.me", "tamara@hytham.me"];
|
||||||
|
from: "Manoon Oils <support@mail.manoonoils.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Should be**:
|
||||||
|
```typescript
|
||||||
|
const ADMIN_EMAILS = process.env.ADMIN_EMAILS?.split(",") || [];
|
||||||
|
from: `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to modify**:
|
||||||
|
- `src/pages/api/webhooks/order-created.ts`
|
||||||
|
- `src/pages/api/webhooks/order-fulfilled.ts`
|
||||||
|
- `src/pages/api/webhooks/order-cancelled.ts`
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Replace hardcoded `ADMIN_EMAILS` with env var
|
||||||
|
- [ ] Replace hardcoded `from` addresses with env vars
|
||||||
|
- [ ] Update `.env.example` with all required variables
|
||||||
|
- [ ] Document env vars in README
|
||||||
|
|
||||||
|
**Effort**: Low
|
||||||
|
**Impact**: Medium (first step toward reusability)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 Fix Resend Daily Quota Issue
|
||||||
|
|
||||||
|
**Problem**: Multiple emails per order (3 customer + 6 admin = 9 emails per order)
|
||||||
|
|
||||||
|
**Solutions to consider**:
|
||||||
|
1. Batch admin emails (send one email with multiple recipients)
|
||||||
|
2. Add option to disable admin emails for testing
|
||||||
|
3. Use BCC for admin emails
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Change admin email sending to use single email with multiple recipients or BCC
|
||||||
|
- [ ] Add `DISABLE_ADMIN_EMAILS` env var for testing
|
||||||
|
- [ ] Consider batching order confirmations
|
||||||
|
|
||||||
|
**Effort**: Low
|
||||||
|
**Impact**: High (saves quota)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 2: Make It Reusable (Foundation)
|
||||||
|
|
||||||
|
### 2.1 Extract Brand Configuration to Env Vars
|
||||||
|
|
||||||
|
**Problem**: 20+ hardcoded Manoon Oils references
|
||||||
|
|
||||||
|
**Files to modify**:
|
||||||
|
- `src/lib/email-templates/order-created.ts`
|
||||||
|
- `src/lib/email-templates/order-shipped.ts`
|
||||||
|
- `src/lib/email-templates/order-cancelled.ts`
|
||||||
|
|
||||||
|
**New env vars needed**:
|
||||||
|
```
|
||||||
|
COMPANY_NAME=Manoon Oils
|
||||||
|
LOGO_URL=https://minio-api.nodecrew.me/...
|
||||||
|
SITE_URL=https://manoonoils.com
|
||||||
|
DASHBOARD_URL=https://dashboard.manoonoils.com
|
||||||
|
TRACKING_URL_TEMPLATE=https://track.manoonoils.com/{trackingNumber}
|
||||||
|
SUPPORT_EMAIL=support@manoonoils.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Replace all hardcoded company names
|
||||||
|
- [ ] Replace all hardcoded URLs
|
||||||
|
- [ ] Replace logo URL
|
||||||
|
- [ ] Update all email templates
|
||||||
|
|
||||||
|
**Effort**: Medium
|
||||||
|
**Impact**: High (enables reusability)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Unify Email Template System
|
||||||
|
|
||||||
|
**Problem**: Two competing systems (React Email vs Handlebars)
|
||||||
|
|
||||||
|
**Decision needed**:
|
||||||
|
- **Option A**: Stick with Handlebars (simpler, already working)
|
||||||
|
- **Option B**: Migrate to React Email (type-safe, better for complex layouts)
|
||||||
|
- **Option C**: Support both (more complex)
|
||||||
|
|
||||||
|
**Recommendation**: Stick with Handlebars for now (it's working), document the React Email files as "future improvement"
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Document the two systems in README
|
||||||
|
- [ ] Remove unused React Email code OR move to separate folder
|
||||||
|
- [ ] Keep Handlebars as primary system
|
||||||
|
|
||||||
|
**Effort**: Low
|
||||||
|
**Impact**: Low (cleanup)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 3: Advanced Features
|
||||||
|
|
||||||
|
### 3.1 Configuration via Saleor Metadata
|
||||||
|
|
||||||
|
**Problem**: Currently requires code changes to customize
|
||||||
|
|
||||||
|
**Solution**: Store configuration in Saleor's app metadata
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- No code changes needed for new stores
|
||||||
|
- Configurable via Saleor dashboard
|
||||||
|
- Single deployment serves multiple stores
|
||||||
|
|
||||||
|
**Files to modify**:
|
||||||
|
- Add configuration service
|
||||||
|
- Modify all webhook handlers to fetch config
|
||||||
|
- Add settings UI in app
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Create configuration interface
|
||||||
|
- [ ] Add GraphQL queries to fetch config from Saleor metadata
|
||||||
|
- [ ] Update webhook handlers to use dynamic config
|
||||||
|
- [ ] Create settings page in app
|
||||||
|
- [ ] Cache configuration to avoid API calls on every webhook
|
||||||
|
|
||||||
|
**Effort**: High
|
||||||
|
**Impact**: High (true multi-tenancy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 Add Email Preview/Test Feature
|
||||||
|
|
||||||
|
**Problem**: Hard to test email templates without creating real orders
|
||||||
|
|
||||||
|
**Solution**: Add API endpoint to preview/test emails
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Create `/api/test-email` endpoint
|
||||||
|
- [ ] Allow sending test emails to any address
|
||||||
|
- [ ] Include sample order data for preview
|
||||||
|
- [ ] Add to app UI for easy testing
|
||||||
|
|
||||||
|
**Effort**: Medium
|
||||||
|
**Impact**: Medium (developer experience)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 Phone Number Validation
|
||||||
|
|
||||||
|
**Problem**: Phone validation failing for some formats (GB numbers rejected)
|
||||||
|
|
||||||
|
**Solution**: Make phone optional OR improve validation
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Investigate Saleor's phone validation
|
||||||
|
- [ ] Either fix validation or make phone optional in templates
|
||||||
|
- [ ] Update GraphQL fragments if needed
|
||||||
|
|
||||||
|
**Effort**: Low
|
||||||
|
**Impact**: Low (edge case)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 4: Documentation & Deployment
|
||||||
|
|
||||||
|
### 4.1 Comprehensive README
|
||||||
|
|
||||||
|
**Sections needed**:
|
||||||
|
- Installation instructions
|
||||||
|
- Environment variables reference
|
||||||
|
- Webhook configuration guide
|
||||||
|
- Testing instructions
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
### 4.2 Docker Compose for Development
|
||||||
|
|
||||||
|
- Local Saleor instance for testing
|
||||||
|
- Easy development setup
|
||||||
|
|
||||||
|
### 4.3 CI/CD Pipeline
|
||||||
|
|
||||||
|
- Automated testing
|
||||||
|
- Automated deployment
|
||||||
|
- Version tagging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Recommended Order of Work
|
||||||
|
|
||||||
|
### Next (Do First):
|
||||||
|
1. **Fix Customer Name in Fulfilled/Cancelled** - Broken UX, high impact
|
||||||
|
2. **Fix Env Var Usage** - Quick win, enables reusability
|
||||||
|
3. **Fix Quota Issue** - Save money immediately
|
||||||
|
|
||||||
|
### Then:
|
||||||
|
4. **Extract Brand Config** - Makes it reusable
|
||||||
|
5. **Documentation** - Helps with future work
|
||||||
|
|
||||||
|
### Later:
|
||||||
|
6. **Saleor Metadata Config** - True multi-tenancy
|
||||||
|
7. **Email Preview Feature** - Nice to have
|
||||||
|
8. **Advanced Features** - When needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
### Current Hardcoded Values
|
||||||
|
|
||||||
|
#### Brand/Company Identity:
|
||||||
|
- Company name: "ManoonOils" (12+ instances)
|
||||||
|
- Logo URL: MinIO URL to Manoon logo
|
||||||
|
- Support email: support@manoonoils.com
|
||||||
|
|
||||||
|
#### URLs:
|
||||||
|
- Site: https://manoonoils.com
|
||||||
|
- Dashboard: https://dashboard.manoonoils.com/orders
|
||||||
|
- Tracking: https://track.manoonoils.com/{trackingNumber}
|
||||||
|
|
||||||
|
#### Email Addresses:
|
||||||
|
- Admin recipients: me@hytham.me, tamara@hytham.me
|
||||||
|
- From address: Manoon Oils <support@mail.manoonoils.com>
|
||||||
|
|
||||||
|
### Two Email Systems
|
||||||
|
|
||||||
|
1. **React Email** (`/src/emails/*.tsx`)
|
||||||
|
- Type-safe React components
|
||||||
|
- Currently unused in webhooks
|
||||||
|
- Good for complex layouts
|
||||||
|
|
||||||
|
2. **Handlebars** (`/src/lib/email-templates/*.ts`)
|
||||||
|
- Simple string templates
|
||||||
|
- Currently used by webhooks
|
||||||
|
- Easy to customize with translations
|
||||||
|
|
||||||
|
**Recommendation**: Stick with Handlebars for production use
|
||||||
|
|
||||||
|
### Webhook Data Inconsistency
|
||||||
|
|
||||||
|
Saleor sends different data for different events:
|
||||||
|
|
||||||
|
- **ORDER_CREATED**: Full order data ✅
|
||||||
|
- **ORDER_FULFILLED**: Partial data (missing shippingAddress details, languageCode) ❌
|
||||||
|
- **ORDER_CANCELLED**: Partial data (missing shippingAddress details, languageCode) ❌
|
||||||
|
|
||||||
|
**Fix**: Query Saleor API in fulfilled/cancelled webhooks to fetch complete order data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables Reference
|
||||||
|
|
||||||
|
### Current (Working):
|
||||||
|
```bash
|
||||||
|
RESEND_API_KEY= # Required - Resend API key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Needed (Not implemented):
|
||||||
|
```bash
|
||||||
|
ADMIN_EMAILS= # Comma-separated admin emails
|
||||||
|
FROM_EMAIL= # Sender email address
|
||||||
|
FROM_NAME= # Sender display name
|
||||||
|
COMPANY_NAME= # Company/brand name
|
||||||
|
LOGO_URL= # URL to logo image
|
||||||
|
SITE_URL= # Customer-facing storefront URL
|
||||||
|
DASHBOARD_URL= # Admin dashboard URL
|
||||||
|
TRACKING_URL_TEMPLATE= # Tracking URL with {trackingNumber} placeholder
|
||||||
|
SUPPORT_EMAIL= # Support email shown in templates
|
||||||
|
DISABLE_ADMIN_EMAILS= # Set to "true" to disable admin notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: March 28, 2026*
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@saleor/app-sdk": "1.5.0",
|
"@saleor/app-sdk": "1.5.0",
|
||||||
"@saleor/macaw-ui": "1.4.1",
|
"@saleor/macaw-ui": "1.4.1",
|
||||||
"@urql/exchange-auth": "^1.0.0",
|
"@urql/exchange-auth": "^1.0.0",
|
||||||
|
"handlebars": "^4.7.9",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
|
|||||||
Generated
+36
-2
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@urql/exchange-auth':
|
'@urql/exchange-auth':
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0(graphql@16.8.1)
|
version: 1.0.0(graphql@16.8.1)
|
||||||
|
handlebars:
|
||||||
|
specifier: ^4.7.9
|
||||||
|
version: 4.7.9
|
||||||
next:
|
next:
|
||||||
specifier: 15.5.9
|
specifier: 15.5.9
|
||||||
version: 15.5.9(@babel/core@7.24.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 15.5.9(@babel/core@7.24.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -3302,6 +3305,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
|
resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
|
||||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||||
|
|
||||||
|
handlebars@4.7.9:
|
||||||
|
resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
|
||||||
|
engines: {node: '>=0.4.7'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
has-bigints@1.0.2:
|
has-bigints@1.0.2:
|
||||||
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||||
|
|
||||||
@@ -3865,6 +3873,9 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
neo-async@2.6.2:
|
||||||
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
|
|
||||||
next@15.5.9:
|
next@15.5.9:
|
||||||
resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
|
resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
|
||||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||||
@@ -4735,6 +4746,11 @@ packages:
|
|||||||
ufo@1.5.3:
|
ufo@1.5.3:
|
||||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||||
|
|
||||||
|
uglify-js@3.19.3:
|
||||||
|
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||||
|
engines: {node: '>=0.8.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
unbox-primitive@1.0.2:
|
unbox-primitive@1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
|
|
||||||
@@ -4951,6 +4967,9 @@ packages:
|
|||||||
wonka@6.3.4:
|
wonka@6.3.4:
|
||||||
resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==}
|
resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==}
|
||||||
|
|
||||||
|
wordwrap@1.0.0:
|
||||||
|
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||||
|
|
||||||
wrap-ansi@6.2.0:
|
wrap-ansi@6.2.0:
|
||||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -8769,6 +8788,15 @@ snapshots:
|
|||||||
|
|
||||||
graphql@16.8.1: {}
|
graphql@16.8.1: {}
|
||||||
|
|
||||||
|
handlebars@4.7.9:
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.8
|
||||||
|
neo-async: 2.6.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
wordwrap: 1.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
uglify-js: 3.19.3
|
||||||
|
|
||||||
has-bigints@1.0.2: {}
|
has-bigints@1.0.2: {}
|
||||||
|
|
||||||
has-flag@3.0.0: {}
|
has-flag@3.0.0: {}
|
||||||
@@ -9357,6 +9385,8 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
next@15.5.9(@babel/core@7.24.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
next@15.5.9(@babel/core@7.24.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.5.9
|
'@next/env': 15.5.9
|
||||||
@@ -9995,8 +10025,7 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.0: {}
|
source-map-js@1.2.0: {}
|
||||||
|
|
||||||
source-map@0.6.1:
|
source-map@0.6.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
spawndamnit@2.0.0:
|
spawndamnit@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -10249,6 +10278,9 @@ snapshots:
|
|||||||
|
|
||||||
ufo@1.5.3: {}
|
ufo@1.5.3: {}
|
||||||
|
|
||||||
|
uglify-js@3.19.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
unbox-primitive@1.0.2:
|
unbox-primitive@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
@@ -10492,6 +10524,8 @@ snapshots:
|
|||||||
|
|
||||||
wonka@6.3.4: {}
|
wonka@6.3.4: {}
|
||||||
|
|
||||||
|
wordwrap@1.0.0: {}
|
||||||
|
|
||||||
wrap-ansi@6.2.0:
|
wrap-ansi@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { getOrderCreatedEmails } from "./order-created";
|
||||||
|
export { getOrderShippedEmails } from "./order-shipped";
|
||||||
|
export { getOrderCancelledEmails } from "./order-cancelled";
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import Handlebars from "handlebars";
|
||||||
|
|
||||||
|
// Translations for Order Cancelled emails
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
subject: "Order {{orderNumber}} Cancelled",
|
||||||
|
title: "Order Cancelled",
|
||||||
|
greeting: "Dear {{customerName}},",
|
||||||
|
body: "Your order has been cancelled. If you have any questions, please contact us.",
|
||||||
|
orderLabel: "Order:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
reasonLabel: "Reason:",
|
||||||
|
noReason: "No reason provided",
|
||||||
|
itemsLabel: "Items",
|
||||||
|
contactButton: "Contact Us",
|
||||||
|
thankYou: "Thank you!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
sr: {
|
||||||
|
subject: "Porudžbina {{orderNumber}} Otkazana",
|
||||||
|
title: "Porudžbina Otkazana",
|
||||||
|
greeting: "Poštovani {{customerName}},",
|
||||||
|
body: "Vaša porudžbina je otkazana. Ako imate pitanja, molimo vas da nas kontaktirate.",
|
||||||
|
orderLabel: "Porudžbina:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
reasonLabel: "Razlog:",
|
||||||
|
noReason: "Razlog nije naveden",
|
||||||
|
itemsLabel: "Artikli",
|
||||||
|
contactButton: "Kontaktirajte Nas",
|
||||||
|
thankYou: "Hvala vam!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
subject: "Bestellung {{orderNumber}} Storniert",
|
||||||
|
title: "Bestellung Storniert",
|
||||||
|
greeting: "Sehr geehrte/r {{customerName}},",
|
||||||
|
body: "Ihre Bestellung wurde storniert. Bei Fragen kontaktieren Sie uns bitte.",
|
||||||
|
orderLabel: "Bestellung:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
reasonLabel: "Grund:",
|
||||||
|
noReason: "Kein Grund angegeben",
|
||||||
|
itemsLabel: "Artikel",
|
||||||
|
contactButton: "Kontaktieren Sie Uns",
|
||||||
|
thankYou: "Vielen Dank!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: "Commande {{orderNumber}} Annulée",
|
||||||
|
title: "Commande Annulée",
|
||||||
|
greeting: "Cher/Chère {{customerName}},",
|
||||||
|
body: "Votre commande a été annulée. Si vous avez des questions, veuillez nous contacter.",
|
||||||
|
orderLabel: "Commande:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
reasonLabel: "Raison:",
|
||||||
|
noReason: "Aucune raison fournie",
|
||||||
|
itemsLabel: "Articles",
|
||||||
|
contactButton: "Contactez-Nous",
|
||||||
|
thankYou: "Merci!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Customer email template
|
||||||
|
const customerEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<div style="text-align:center;margin-bottom:30px;">
|
||||||
|
<img src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" width="150" alt="{{companyName}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
||||||
|
|
||||||
|
<p style="font-size:16px;color:#333;margin-bottom:10px;">{{greeting}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:20px;">{{body}}</p>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{orderLabel}}</strong> {{orderNumber}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{reasonLabel}}</strong> {{cancellationReason}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="https://manoonoils.com/contact" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{contactButton}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
||||||
|
<p style="font-size:14px;font-weight:bold;color:#1a1a1a;">{{thankYou}}</p>
|
||||||
|
|
||||||
|
<div style="margin-top:40px;padding-top:20px;border-top:1px solid #e0e0e0;text-align:center;color:#666;font-size:12px;">
|
||||||
|
<p>{{companyName}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Admin email template
|
||||||
|
const adminEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">Order Cancelled</h1>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">Order #{{orderNumber}}</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Date:</strong> {{orderDate}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>Reason:</strong> {{cancellationReason}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">Customer</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Name:</strong> {{customerName}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Email:</strong> {{customerEmail}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>Phone:</strong> {{#if phone}}{{phone}}{{else}}Not provided{{/if}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">Items</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="https://dashboard.manoonoils.com/orders" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Compile templates
|
||||||
|
const compileCustomerEmail = Handlebars.compile(customerEmailTemplate);
|
||||||
|
const compileAdminEmail = Handlebars.compile(adminEmailTemplate);
|
||||||
|
|
||||||
|
export function getOrderCancelledEmails(order: any) {
|
||||||
|
// Detect language
|
||||||
|
const lang = order.languageCode?.toLowerCase() || "en";
|
||||||
|
const t = translations[lang as keyof typeof translations] || translations.en;
|
||||||
|
|
||||||
|
// Format data
|
||||||
|
const customerName = order.shippingAddress?.firstName
|
||||||
|
? `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim()
|
||||||
|
: order.userEmail?.split("@")[0] || "Customer";
|
||||||
|
|
||||||
|
const currency = order.total?.gross?.currency || "EUR";
|
||||||
|
|
||||||
|
const items = (order.lines || []).map((line: any) => ({
|
||||||
|
name: line.variant?.product?.name || line.variant?.name || "Product",
|
||||||
|
quantity: line.quantity || 0,
|
||||||
|
price: `${line.totalPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
const orderDate = order.created
|
||||||
|
? new Date(order.created).toLocaleString(lang === "sr" ? "sr-RS" : lang === "de" ? "de-DE" : lang === "fr" ? "fr-FR" : "en-US")
|
||||||
|
: new Date().toLocaleString();
|
||||||
|
|
||||||
|
const cancellationReason = order.cancellationReason || t.noReason;
|
||||||
|
|
||||||
|
// Customer email data
|
||||||
|
const customerData = {
|
||||||
|
...t,
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
orderDate,
|
||||||
|
cancellationReason,
|
||||||
|
items
|
||||||
|
};
|
||||||
|
|
||||||
|
// Admin email data (English)
|
||||||
|
const adminData = {
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
customerEmail: order.userEmail || "",
|
||||||
|
orderDate,
|
||||||
|
cancellationReason,
|
||||||
|
phone: order.shippingAddress?.phone || "",
|
||||||
|
items
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
customerSubject: Handlebars.compile(t.subject)({ orderNumber: order.number || order.id }),
|
||||||
|
customerHtml: compileCustomerEmail(customerData),
|
||||||
|
adminSubject: `Order Cancelled #${order.number || order.id}`,
|
||||||
|
adminHtml: compileAdminEmail(adminData)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
import Handlebars from "handlebars";
|
||||||
|
|
||||||
|
// Translations for Order Created emails
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
subject: "Order {{orderNumber}} Confirmed",
|
||||||
|
title: "Order Confirmed",
|
||||||
|
greeting: "Dear {{customerName}},",
|
||||||
|
body: "Thank you for your order! We have received it and it is now being processed.",
|
||||||
|
orderLabel: "Order:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
statusLabel: "Status:",
|
||||||
|
itemsLabel: "Items",
|
||||||
|
subtotalLabel: "Subtotal:",
|
||||||
|
shippingLabel: "Shipping:",
|
||||||
|
taxLabel: "Tax:",
|
||||||
|
totalLabel: "Total:",
|
||||||
|
shippingAddressLabel: "Shipping Address",
|
||||||
|
phoneLabel: "Phone:",
|
||||||
|
phoneNotProvided: "Not provided",
|
||||||
|
viewOrderButton: "View Order",
|
||||||
|
thankYou: "Thank you!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
sr: {
|
||||||
|
subject: "Porudžbina {{orderNumber}} Potvrđena",
|
||||||
|
title: "Porudžbina Potvrđena",
|
||||||
|
greeting: "Poštovani {{customerName}},",
|
||||||
|
body: "Hvala vam na porudžbini! Primili smo je i sada je obrađujemo.",
|
||||||
|
orderLabel: "Porudžbina:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
statusLabel: "Status:",
|
||||||
|
itemsLabel: "Artikli",
|
||||||
|
subtotalLabel: "Međuzbir:",
|
||||||
|
shippingLabel: "Dostava:",
|
||||||
|
taxLabel: "Porez:",
|
||||||
|
totalLabel: "Ukupno:",
|
||||||
|
shippingAddressLabel: "Adresa za Dostavu",
|
||||||
|
phoneLabel: "Telefon:",
|
||||||
|
phoneNotProvided: "Nije navedeno",
|
||||||
|
viewOrderButton: "Pogledaj Porudžbinu",
|
||||||
|
thankYou: "Hvala vam!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
subject: "Bestellung {{orderNumber}} Bestätigt",
|
||||||
|
title: "Bestellung Bestätigt",
|
||||||
|
greeting: "Sehr geehrte/r {{customerName}},",
|
||||||
|
body: "Vielen Dank für Ihre Bestellung! Wir haben sie erhalten und bearbeiten sie jetzt.",
|
||||||
|
orderLabel: "Bestellung:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
statusLabel: "Status:",
|
||||||
|
itemsLabel: "Artikel",
|
||||||
|
subtotalLabel: "Zwischensumme:",
|
||||||
|
shippingLabel: "Versand:",
|
||||||
|
taxLabel: "Steuer:",
|
||||||
|
totalLabel: "Gesamt:",
|
||||||
|
shippingAddressLabel: "Lieferadresse",
|
||||||
|
phoneLabel: "Telefon:",
|
||||||
|
phoneNotProvided: "Nicht angegeben",
|
||||||
|
viewOrderButton: "Bestellung Ansehen",
|
||||||
|
thankYou: "Vielen Dank!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: "Commande {{orderNumber}} Confirmée",
|
||||||
|
title: "Commande Confirmée",
|
||||||
|
greeting: "Cher/Chère {{customerName}},",
|
||||||
|
body: "Merci pour votre commande! Nous l'avons reçue et elle est en cours de traitement.",
|
||||||
|
orderLabel: "Commande:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
statusLabel: "Statut:",
|
||||||
|
itemsLabel: "Articles",
|
||||||
|
subtotalLabel: "Sous-total:",
|
||||||
|
shippingLabel: "Livraison:",
|
||||||
|
taxLabel: "Taxe:",
|
||||||
|
totalLabel: "Total:",
|
||||||
|
shippingAddressLabel: "Adresse de Livraison",
|
||||||
|
phoneLabel: "Téléphone:",
|
||||||
|
phoneNotProvided: "Non fourni",
|
||||||
|
viewOrderButton: "Voir la Commande",
|
||||||
|
thankYou: "Merci!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Customer email template
|
||||||
|
const customerEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<div style="text-align:center;margin-bottom:30px;">
|
||||||
|
<img src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" width="150" alt="{{companyName}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
||||||
|
|
||||||
|
<p style="font-size:16px;color:#333;margin-bottom:10px;">{{greeting}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:20px;">{{body}}</p>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{orderLabel}}</strong> {{orderNumber}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{statusLabel}}</strong> {{orderStatus}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{subtotalLabel}}</strong> {{subtotal}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{shippingLabel}}</strong> {{shipping}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{taxLabel}}</strong> {{tax}}</p>
|
||||||
|
<hr style="border-color:#e0e0e0;margin:10px 0;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0;"><strong>{{totalLabel}}</strong> {{total}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if shippingAddress}}
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{shippingAddressLabel}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin:0;white-space:pre-line;">{{shippingAddress}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin:8px 0 0 0;"><strong>{{phoneLabel}}</strong> {{#if phone}}{{phone}}{{else}}{{phoneNotProvided}}{{/if}}</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="https://manoonoils.com" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{viewOrderButton}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
||||||
|
<p style="font-size:14px;font-weight:bold;color:#1a1a1a;">{{thankYou}}</p>
|
||||||
|
|
||||||
|
<div style="margin-top:40px;padding-top:20px;border-top:1px solid #e0e0e0;text-align:center;color:#666;font-size:12px;">
|
||||||
|
<p>{{companyName}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Admin email template
|
||||||
|
const adminEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{adminTitle}}</h1>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">{{adminOrderLabel}} #{{orderNumber}}</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{statusLabel}}</strong> {{orderStatus}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{paymentLabel}}</strong> {{paymentMethod}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">{{customerLabel}}</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{nameLabel}}</strong> {{customerName}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{emailLabel}}</strong> {{customerEmail}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{phoneLabel}}</strong> {{#if phone}}{{phone}}{{else}}{{phoneNotProvided}}{{/if}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{subtotalLabel}}</strong> {{subtotal}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{shippingLabel}}</strong> {{shipping}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{taxLabel}}</strong> {{tax}}</p>
|
||||||
|
<hr style="border-color:#e0e0e0;margin:10px 0;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0;"><strong>{{totalLabel}}</strong> {{total}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if shippingAddress}}
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{shippingAddressLabel}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin:0;white-space:pre-line;">{{shippingAddress}}</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="https://dashboard.manoonoils.com/orders" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Compile templates
|
||||||
|
const compileCustomerEmail = Handlebars.compile(customerEmailTemplate);
|
||||||
|
const compileAdminEmail = Handlebars.compile(adminEmailTemplate);
|
||||||
|
|
||||||
|
// Admin translations (always in English)
|
||||||
|
const adminTranslations = {
|
||||||
|
adminTitle: "New Order! 🎉",
|
||||||
|
adminOrderLabel: "Order",
|
||||||
|
customerLabel: "Customer",
|
||||||
|
paymentLabel: "Payment:",
|
||||||
|
paymentMethod: "Card"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getOrderCreatedEmails(order: any) {
|
||||||
|
// Detect language
|
||||||
|
const lang = order.languageCode?.toLowerCase() || "en";
|
||||||
|
const t = translations[lang as keyof typeof translations] || translations.en;
|
||||||
|
|
||||||
|
// Format data
|
||||||
|
const customerName = order.shippingAddress?.firstName
|
||||||
|
? `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim()
|
||||||
|
: order.userEmail?.split("@")[0] || "Customer";
|
||||||
|
|
||||||
|
const currency = order.total?.gross?.currency || "EUR";
|
||||||
|
|
||||||
|
const items = (order.lines || []).map((line: any) => ({
|
||||||
|
name: line.variant?.product?.name || line.variant?.name || "Product",
|
||||||
|
quantity: line.quantity || 0,
|
||||||
|
price: `${line.totalPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
const shippingAddress = order.shippingAddress
|
||||||
|
? `${order.shippingAddress.firstName || ""} ${order.shippingAddress.lastName || ""}\n${order.shippingAddress.streetAddress1 || ""}\n${order.shippingAddress.city || ""}, ${order.shippingAddress.postalCode || ""}\n${order.shippingAddress.country?.country || ""}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const orderDate = order.created
|
||||||
|
? new Date(order.created).toLocaleString(lang === "sr" ? "sr-RS" : lang === "de" ? "de-DE" : lang === "fr" ? "fr-FR" : "en-US")
|
||||||
|
: new Date().toLocaleString();
|
||||||
|
|
||||||
|
// Customer email data
|
||||||
|
const customerData = {
|
||||||
|
...t,
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
orderDate,
|
||||||
|
orderStatus: order.status || "unfulfilled",
|
||||||
|
items,
|
||||||
|
subtotal: `${order.subtotal?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
shipping: `${order.shippingPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
tax: `${order.total?.tax?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
total: `${order.total?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
shippingAddress,
|
||||||
|
phone: order.shippingAddress?.phone || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Admin email data (English)
|
||||||
|
const adminData = {
|
||||||
|
...t,
|
||||||
|
...adminTranslations,
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
customerEmail: order.userEmail || "",
|
||||||
|
orderDate,
|
||||||
|
orderStatus: order.status || "unfulfilled",
|
||||||
|
items,
|
||||||
|
subtotal: `${order.subtotal?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
shipping: `${order.shippingPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
tax: `${order.total?.tax?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
total: `${order.total?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
||||||
|
shippingAddress,
|
||||||
|
phone: order.shippingAddress?.phone || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
customerSubject: Handlebars.compile(t.subject)({ orderNumber: order.number || order.id }),
|
||||||
|
customerHtml: compileCustomerEmail(customerData),
|
||||||
|
adminSubject: `New Order! 🎉 #${order.number || order.id}`,
|
||||||
|
adminHtml: compileAdminEmail(adminData)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import Handlebars from "handlebars";
|
||||||
|
|
||||||
|
// Translations for Order Shipped emails
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
subject: "Order {{orderNumber}} Has Shipped!",
|
||||||
|
title: "Your Order Has Shipped!",
|
||||||
|
greeting: "Dear {{customerName}},",
|
||||||
|
body: "Great news! Your order has been shipped and is on its way.",
|
||||||
|
orderLabel: "Order:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
trackingLabel: "Tracking Number:",
|
||||||
|
itemsLabel: "Items",
|
||||||
|
trackButton: "Track Your Order",
|
||||||
|
thankYou: "Thank you!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
sr: {
|
||||||
|
subject: "Porudžbina {{orderNumber}} Poslata!",
|
||||||
|
title: "Vaša Porudžbina je Poslata!",
|
||||||
|
greeting: "Poštovani {{customerName}},",
|
||||||
|
body: "Odlične vesti! Vaša porudžbina je poslata i na putu je.",
|
||||||
|
orderLabel: "Porudžbina:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
trackingLabel: "Broj Za Praćenje:",
|
||||||
|
itemsLabel: "Artikli",
|
||||||
|
trackButton: "Pratite Porudžbinu",
|
||||||
|
thankYou: "Hvala vam!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
subject: "Bestellung {{orderNumber}} Wurde Versendet!",
|
||||||
|
title: "Ihre Bestellung Wurde Versendet!",
|
||||||
|
greeting: "Sehr geehrte/r {{customerName}},",
|
||||||
|
body: "Tolle Neuigkeiten! Ihre Bestellung wurde versendet und ist unterwegs.",
|
||||||
|
orderLabel: "Bestellung:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
trackingLabel: "Sendungsnummer:",
|
||||||
|
itemsLabel: "Artikel",
|
||||||
|
trackButton: "Bestellung Verfolgen",
|
||||||
|
thankYou: "Vielen Dank!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: "Commande {{orderNumber}} Expédiée!",
|
||||||
|
title: "Votre Commande a été Expédiée!",
|
||||||
|
greeting: "Cher/Chère {{customerName}},",
|
||||||
|
body: "Bonne nouvelle! Votre commande a été expédiée et est en route.",
|
||||||
|
orderLabel: "Commande:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
trackingLabel: "Numéro de Suivi:",
|
||||||
|
itemsLabel: "Articles",
|
||||||
|
trackButton: "Suivre la Commande",
|
||||||
|
thankYou: "Merci!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Customer email template
|
||||||
|
const customerEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<div style="text-align:center;margin-bottom:30px;">
|
||||||
|
<img src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" width="150" alt="{{companyName}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
||||||
|
|
||||||
|
<p style="font-size:16px;color:#333;margin-bottom:10px;">{{greeting}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:20px;">{{body}}</p>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{orderLabel}}</strong> {{orderNumber}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
||||||
|
{{#if trackingNumber}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{trackingLabel}}</strong> {{trackingNumber}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if trackingUrl}}
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="{{trackingUrl}}" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{trackButton}}</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
||||||
|
<p style="font-size:14px;font-weight:bold;color:#1a1a1a;">{{thankYou}}</p>
|
||||||
|
|
||||||
|
<div style="margin-top:40px;padding-top:20px;border-top:1px solid #e0e0e0;text-align:center;color:#666;font-size:12px;">
|
||||||
|
<p>{{companyName}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Admin email template
|
||||||
|
const adminEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">Order Shipped</h1>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">Order #{{orderNumber}}</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Date:</strong> {{orderDate}}</p>
|
||||||
|
{{#if trackingNumber}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>Tracking:</strong> {{trackingNumber}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">Customer</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Name:</strong> {{customerName}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Email:</strong> {{customerEmail}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>Phone:</strong> {{#if phone}}{{phone}}{{else}}Not provided{{/if}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">Items</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="https://dashboard.manoonoils.com/orders" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Compile templates
|
||||||
|
const compileCustomerEmail = Handlebars.compile(customerEmailTemplate);
|
||||||
|
const compileAdminEmail = Handlebars.compile(adminEmailTemplate);
|
||||||
|
|
||||||
|
export function getOrderShippedEmails(order: any, fulfillment?: any) {
|
||||||
|
// Detect language
|
||||||
|
const lang = order.languageCode?.toLowerCase() || "en";
|
||||||
|
const t = translations[lang as keyof typeof translations] || translations.en;
|
||||||
|
|
||||||
|
// Format data
|
||||||
|
const customerName = order.shippingAddress?.firstName
|
||||||
|
? `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim()
|
||||||
|
: order.userEmail?.split("@")[0] || "Customer";
|
||||||
|
|
||||||
|
const currency = order.total?.gross?.currency || "EUR";
|
||||||
|
|
||||||
|
const items = (order.lines || []).map((line: any) => ({
|
||||||
|
name: line.variant?.product?.name || line.variant?.name || "Product",
|
||||||
|
quantity: line.quantity || 0,
|
||||||
|
price: `${line.totalPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
const orderDate = order.created
|
||||||
|
? new Date(order.created).toLocaleString(lang === "sr" ? "sr-RS" : lang === "de" ? "de-DE" : lang === "fr" ? "fr-FR" : "en-US")
|
||||||
|
: new Date().toLocaleString();
|
||||||
|
|
||||||
|
// Get tracking info from fulfillment
|
||||||
|
const trackingNumber = fulfillment?.trackingNumber || "";
|
||||||
|
const trackingUrl = trackingNumber
|
||||||
|
? `https://track.manoonoils.com/${trackingNumber}`
|
||||||
|
: "https://manoonoils.com";
|
||||||
|
|
||||||
|
// Customer email data
|
||||||
|
const customerData = {
|
||||||
|
...t,
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
orderDate,
|
||||||
|
trackingNumber,
|
||||||
|
trackingUrl,
|
||||||
|
items
|
||||||
|
};
|
||||||
|
|
||||||
|
// Admin email data (English)
|
||||||
|
const adminData = {
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
customerEmail: order.userEmail || "",
|
||||||
|
orderDate,
|
||||||
|
trackingNumber,
|
||||||
|
phone: order.shippingAddress?.phone || "",
|
||||||
|
items
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
customerSubject: Handlebars.compile(t.subject)({ orderNumber: order.number || order.id }),
|
||||||
|
customerHtml: compileCustomerEmail(customerData),
|
||||||
|
adminSubject: `Order Shipped #${order.number || order.id}`,
|
||||||
|
adminHtml: compileAdminEmail(adminData)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@ import {
|
|||||||
OrderCancelledWebhookPayloadFragment,
|
OrderCancelledWebhookPayloadFragment,
|
||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
|
import { getOrderCancelledEmails } from "@/lib/email-templates";
|
||||||
|
import { Resend } from "resend";
|
||||||
|
|
||||||
// N8N webhook URL for Order Cancelled
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
const N8N_WEBHOOK_URL = "https://n8n.nodecrew.me/webhook/saleor-order-cancelled";
|
|
||||||
|
const ADMIN_EMAILS = ["me@hytham.me", "tamara@hytham.me"];
|
||||||
|
|
||||||
export const orderCancelledWebhook = new SaleorAsyncWebhook<OrderCancelledWebhookPayloadFragment>({
|
export const orderCancelledWebhook = new SaleorAsyncWebhook<OrderCancelledWebhookPayloadFragment>({
|
||||||
name: "Order Cancelled in Saleor",
|
name: "Order Cancelled in Saleor",
|
||||||
@@ -17,29 +20,43 @@ export const orderCancelledWebhook = new SaleorAsyncWebhook<OrderCancelledWebhoo
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default orderCancelledWebhook.createHandler(async (req, res, ctx) => {
|
export default orderCancelledWebhook.createHandler(async (req, res, ctx) => {
|
||||||
const { payload, event, authData } = ctx;
|
const { payload } = ctx;
|
||||||
|
const order = payload.order;
|
||||||
|
|
||||||
console.log(`Order cancelled: ${payload.order?.number} for ${payload.order?.userEmail}`);
|
if (!order) {
|
||||||
console.log(`Forwarding to N8N: ${N8N_WEBHOOK_URL}`);
|
console.error("No order data in webhook payload");
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Order cancelled: ${order.number} for ${order.userEmail} (lang: ${order.languageCode})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Forward to N8N
|
// Get email templates
|
||||||
const response = await fetch(N8N_WEBHOOK_URL, {
|
const { customerSubject, customerHtml, adminSubject, adminHtml } = getOrderCancelledEmails(order);
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-saleor-event": "order.cancelled",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
// Send customer email
|
||||||
console.error(`N8N returned ${response.status}: ${await response.text()}`);
|
if (order.userEmail) {
|
||||||
} else {
|
const customerResult = await resend.emails.send({
|
||||||
console.log(`Successfully forwarded to N8N: ${response.status}`);
|
from: "Manoon Oils <support@mail.manoonoils.com>",
|
||||||
|
to: order.userEmail,
|
||||||
|
subject: customerSubject,
|
||||||
|
html: customerHtml,
|
||||||
|
});
|
||||||
|
console.log(`Customer cancelled email sent: ${customerResult.data?.id || 'failed'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send admin emails
|
||||||
|
for (const adminEmail of ADMIN_EMAILS) {
|
||||||
|
const adminResult = await resend.emails.send({
|
||||||
|
from: "Manoon Oils <support@mail.manoonoils.com>",
|
||||||
|
to: adminEmail,
|
||||||
|
subject: adminSubject,
|
||||||
|
html: adminHtml,
|
||||||
|
});
|
||||||
|
console.log(`Admin cancelled email sent to ${adminEmail}: ${adminResult.data?.id || 'failed'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to forward to N8N:`, error);
|
console.error("Failed to send cancelled emails:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import {
|
|||||||
OrderCreatedWebhookPayloadFragment,
|
OrderCreatedWebhookPayloadFragment,
|
||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
|
import { getOrderCreatedEmails } from "@/lib/email-templates";
|
||||||
|
import { Resend } from "resend";
|
||||||
|
|
||||||
// N8N webhook URL for Order Created
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
const N8N_WEBHOOK_URL = "https://n8n.nodecrew.me/webhook/saleor-order-created";
|
|
||||||
|
const ADMIN_EMAILS = ["me@hytham.me", "tamara@hytham.me"];
|
||||||
|
|
||||||
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPayloadFragment>({
|
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPayloadFragment>({
|
||||||
name: "Order Created in Saleor",
|
name: "Order Created in Saleor",
|
||||||
@@ -17,29 +20,43 @@ export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPay
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default orderCreatedWebhook.createHandler(async (req, res, ctx) => {
|
export default orderCreatedWebhook.createHandler(async (req, res, ctx) => {
|
||||||
const { payload, event, authData } = ctx;
|
const { payload } = ctx;
|
||||||
|
const order = payload.order;
|
||||||
|
|
||||||
console.log(`Order created: ${payload.order?.number} for ${payload.order?.userEmail}`);
|
if (!order) {
|
||||||
console.log(`Forwarding to N8N: ${N8N_WEBHOOK_URL}`);
|
console.error("No order data in webhook payload");
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Order created: ${order.number} for ${order.userEmail} (lang: ${order.languageCode})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Forward to N8N
|
// Get email templates
|
||||||
const response = await fetch(N8N_WEBHOOK_URL, {
|
const { customerSubject, customerHtml, adminSubject, adminHtml } = getOrderCreatedEmails(order);
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-saleor-event": "order.created",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
// Send customer email
|
||||||
console.error(`N8N returned ${response.status}: ${await response.text()}`);
|
if (order.userEmail) {
|
||||||
} else {
|
const customerResult = await resend.emails.send({
|
||||||
console.log(`Successfully forwarded to N8N: ${response.status}`);
|
from: "Manoon Oils <support@mail.manoonoils.com>",
|
||||||
|
to: order.userEmail,
|
||||||
|
subject: customerSubject,
|
||||||
|
html: customerHtml,
|
||||||
|
});
|
||||||
|
console.log(`Customer email sent: ${customerResult.data?.id || 'failed'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send admin emails
|
||||||
|
for (const adminEmail of ADMIN_EMAILS) {
|
||||||
|
const adminResult = await resend.emails.send({
|
||||||
|
from: "Manoon Oils <support@mail.manoonoils.com>",
|
||||||
|
to: adminEmail,
|
||||||
|
subject: adminSubject,
|
||||||
|
html: adminHtml,
|
||||||
|
});
|
||||||
|
console.log(`Admin email sent to ${adminEmail}: ${adminResult.data?.id || 'failed'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to forward to N8N:`, error);
|
console.error("Failed to send emails:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import {
|
|||||||
OrderFulfilledWebhookPayloadFragment,
|
OrderFulfilledWebhookPayloadFragment,
|
||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
|
import { getOrderShippedEmails } from "@/lib/email-templates";
|
||||||
|
import { Resend } from "resend";
|
||||||
|
|
||||||
// N8N webhook URL for Order Fulfilled/Shipped
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
const N8N_WEBHOOK_URL = "https://n8n.nodecrew.me/webhook/saleor-order-shipped";
|
|
||||||
|
const ADMIN_EMAILS = ["me@hytham.me", "tamara@hytham.me"];
|
||||||
|
|
||||||
export const orderFulfilledWebhook = new SaleorAsyncWebhook<OrderFulfilledWebhookPayloadFragment>({
|
export const orderFulfilledWebhook = new SaleorAsyncWebhook<OrderFulfilledWebhookPayloadFragment>({
|
||||||
name: "Order Fulfilled in Saleor",
|
name: "Order Fulfilled in Saleor",
|
||||||
@@ -17,29 +20,47 @@ export const orderFulfilledWebhook = new SaleorAsyncWebhook<OrderFulfilledWebhoo
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default orderFulfilledWebhook.createHandler(async (req, res, ctx) => {
|
export default orderFulfilledWebhook.createHandler(async (req, res, ctx) => {
|
||||||
const { payload, event, authData } = ctx;
|
const { payload } = ctx;
|
||||||
|
const order = payload.order;
|
||||||
|
|
||||||
console.log(`Order fulfilled: ${payload.order?.number} for ${payload.order?.userEmail}`);
|
if (!order) {
|
||||||
console.log(`Forwarding to N8N: ${N8N_WEBHOOK_URL}`);
|
console.error("No order data in webhook payload");
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first fulfillment with tracking info
|
||||||
|
const fulfillment = (order as any).fulfillments?.[0];
|
||||||
|
const trackingNumber = fulfillment?.trackingNumber || "";
|
||||||
|
|
||||||
|
console.log(`Order fulfilled: ${order.number} for ${order.userEmail} (lang: ${order.languageCode}, tracking: ${trackingNumber})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Forward to N8N
|
// Get email templates
|
||||||
const response = await fetch(N8N_WEBHOOK_URL, {
|
const { customerSubject, customerHtml, adminSubject, adminHtml } = getOrderShippedEmails(order, fulfillment);
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-saleor-event": "order.fulfilled",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
// Send customer email
|
||||||
console.error(`N8N returned ${response.status}: ${await response.text()}`);
|
if (order.userEmail) {
|
||||||
} else {
|
const customerResult = await resend.emails.send({
|
||||||
console.log(`Successfully forwarded to N8N: ${response.status}`);
|
from: "Manoon Oils <support@mail.manoonoils.com>",
|
||||||
|
to: order.userEmail,
|
||||||
|
subject: customerSubject,
|
||||||
|
html: customerHtml,
|
||||||
|
});
|
||||||
|
console.log(`Customer shipped email sent: ${customerResult.data?.id || 'failed'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send admin emails
|
||||||
|
for (const adminEmail of ADMIN_EMAILS) {
|
||||||
|
const adminResult = await resend.emails.send({
|
||||||
|
from: "Manoon Oils <support@mail.manoonoils.com>",
|
||||||
|
to: adminEmail,
|
||||||
|
subject: adminSubject,
|
||||||
|
html: adminHtml,
|
||||||
|
});
|
||||||
|
console.log(`Admin shipped email sent to ${adminEmail}: ${adminResult.data?.id || 'failed'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to forward to N8N:`, error);
|
console.error("Failed to send shipped emails:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
Reference in New Issue
Block a user