@gnar-engine/cli 1.0.5 → 1.0.7
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/bootstrap/deploy.localdev.yml +14 -0
- package/bootstrap/secrets.localdev.yml +7 -3
- package/bootstrap/services/notification/Dockerfile +2 -2
- package/bootstrap/services/notification/package.json +14 -32
- package/bootstrap/services/notification/src/app.js +50 -48
- package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
- package/bootstrap/services/notification/src/config.js +55 -12
- package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
- package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
- package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
- package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
- package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
- package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
- package/bootstrap/services/notification/src/services/notification.service.js +32 -0
- package/bootstrap/services/portal/src/services/client.js +3 -0
- package/bootstrap/services/user/src/commands/user.handler.js +35 -18
- package/bootstrap/services/user/src/tests/commands/user.test.js +15 -6
- package/install-from-clone.sh +1 -1
- package/package.json +1 -1
- package/src/cli.js +0 -6
- package/src/config.js +13 -1
- package/src/dev/commands.js +7 -3
- package/src/dev/dev.service.js +192 -128
- package/src/helpers/helpers.js +24 -0
- package/src/profiles/command.js +41 -0
- package/src/profiles/profiles.client.js +23 -0
- package/src/scaffolder/commands.js +57 -1
- package/src/scaffolder/scaffolder.handler.js +127 -60
- package/src/services/docker.js +173 -0
- package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
- package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
- package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
- package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
- package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
- package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
- package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
- package/bootstrap/services/notification/Dockerfile.prod +0 -37
- package/bootstrap/services/notification/README.md +0 -3
- package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
- package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
- package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
- package/bootstrap/services/notification/src/services/logger.service.js +0 -16
- package/bootstrap/services/notification/src/services/ses.service.js +0 -23
- package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
- package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
- package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
- package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
- package/bootstrap/services/notification/src/tests/notification.test.js +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { db, logger } from '@gnar-engine/core';
|
|
2
|
+
import { ObjectId } from 'mongodb';
|
|
3
|
+
|
|
4
|
+
export const {{entityName}} = {
|
|
5
|
+
|
|
6
|
+
// Get all {{lowerCasePlural entityName}}
|
|
7
|
+
getAll: async () => {
|
|
8
|
+
try {
|
|
9
|
+
const items = await db.collection('{{lowerCasePlural entityName}}').find().toArray();
|
|
10
|
+
return items;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
logger.error("Error fetching {{lowerCasePlural entityName}}:", error);
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Create a {{lowerCase entityName}}
|
|
18
|
+
create: async (data) => {
|
|
19
|
+
try {
|
|
20
|
+
const collection = db.collection('{{lowerCasePlural entityName}}');
|
|
21
|
+
const result = await collection.insertOne(data);
|
|
22
|
+
return await collection.findOne({ _id: result.insertedId });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logger.error("Error creating {{lowerCase entityName}}:", error);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Get a {{lowerCase entityName}} by ID
|
|
30
|
+
getById: async ({ id }) => {
|
|
31
|
+
try {
|
|
32
|
+
const collection = db.collection('{{lowerCasePlural entityName}}');
|
|
33
|
+
const objectId = new ObjectId(id);
|
|
34
|
+
const item = await collection.findOne({ _id: objectId });
|
|
35
|
+
return item;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.error("Error fetching {{lowerCase entityName}}:", error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Update a {{lowerCase entityName}}
|
|
43
|
+
update: async ({ id, updatedData }) => {
|
|
44
|
+
try {
|
|
45
|
+
const collection = db.collection('{{lowerCasePlural entityName}}');
|
|
46
|
+
const objectId = new ObjectId(id);
|
|
47
|
+
const result = await collection.updateOne(
|
|
48
|
+
{ _id: objectId },
|
|
49
|
+
{ $set: updatedData }
|
|
50
|
+
);
|
|
51
|
+
return result.modifiedCount > 0;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error("Error updating {{lowerCase entityName}}:", error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Delete a {{lowerCase entityName}}
|
|
59
|
+
delete: async ({ id }) => {
|
|
60
|
+
try {
|
|
61
|
+
const collection = db.collection('{{lowerCasePlural entityName}}');
|
|
62
|
+
const objectId = new ObjectId(id);
|
|
63
|
+
const result = await collection.deleteOne({ _id: objectId });
|
|
64
|
+
return result.deletedCount > 0;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.error("Error deleting {{lowerCase entityName}}:", error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { db } from '@gnar-engine/core';
|
|
2
|
+
|
|
3
|
+
export const {{entityName}} = {
|
|
4
|
+
async getById({ id }) {
|
|
5
|
+
const [result] = await db.query('SELECT * FROM {{lowerCasePlural entityName}} WHERE id = ?', [id]);
|
|
6
|
+
return result || null;
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async getAll() {
|
|
10
|
+
return await db.query('SELECT * FROM {{lowerCasePlural entityName}}');
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async create(data) {
|
|
14
|
+
const { insertId } = await db.query('INSERT INTO {{lowerCasePlural entityName}} (created_at, updated_at) VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)');
|
|
15
|
+
return await this.getById({ id: insertId });
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async update({ id, ...data }) {
|
|
19
|
+
await db.query('UPDATE {{lowerCasePlural entityName}} SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id]);
|
|
20
|
+
return await this.getById({ id });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async delete({ id }) {
|
|
24
|
+
await db.query('DELETE FROM {{lowerCasePlural entityName}} WHERE id = ?', [id]);
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# Stage 1: Builder
|
|
2
|
-
FROM node:20-alpine AS builder
|
|
3
|
-
|
|
4
|
-
WORKDIR /app
|
|
5
|
-
|
|
6
|
-
# Copy app source
|
|
7
|
-
COPY ./services/notification/src ./src
|
|
8
|
-
COPY ./Lib ./Lib
|
|
9
|
-
|
|
10
|
-
# Copy environment variables (do this later in build only if needed)
|
|
11
|
-
COPY ./.env.production .env
|
|
12
|
-
|
|
13
|
-
# Copy package files and install deps
|
|
14
|
-
COPY ./services/notification/package*.json ./
|
|
15
|
-
RUN npm install --omit=dev
|
|
16
|
-
|
|
17
|
-
# Stage 2: Runtime
|
|
18
|
-
FROM node:20-alpine
|
|
19
|
-
|
|
20
|
-
WORKDIR /app
|
|
21
|
-
|
|
22
|
-
# Copy built app from builder stage
|
|
23
|
-
COPY --from=builder /app /app
|
|
24
|
-
|
|
25
|
-
# Install system deps
|
|
26
|
-
RUN apk add --no-cache ca-certificates wget
|
|
27
|
-
|
|
28
|
-
# Install AWS DocumentDB CA certificates
|
|
29
|
-
RUN mkdir -p /usr/local/share/ca-certificates && \
|
|
30
|
-
wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -O /usr/local/share/ca-certificates/aws-docdb.pem && \
|
|
31
|
-
update-ca-certificates
|
|
32
|
-
|
|
33
|
-
# Expose port
|
|
34
|
-
EXPOSE 4000
|
|
35
|
-
|
|
36
|
-
# Start app
|
|
37
|
-
CMD ["npm", "run", "start"]
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command bus
|
|
3
|
-
*/
|
|
4
|
-
export const commandBus = {
|
|
5
|
-
handlers: new Map(),
|
|
6
|
-
|
|
7
|
-
register(commandName, handlerFunction) {
|
|
8
|
-
this.handlers.set(commandName, handlerFunction);
|
|
9
|
-
},
|
|
10
|
-
|
|
11
|
-
async execute(commandName, ...args) {
|
|
12
|
-
const handlerFunction = this.handlers.get(commandName);
|
|
13
|
-
if (!handlerFunction) {
|
|
14
|
-
console.log('handlers', this.handlers);
|
|
15
|
-
throw new Error(`Command ${commandName} not registered`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return await handlerFunction(...args);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Run seeders
|
|
4
|
-
*
|
|
5
|
-
* @param {Object} params
|
|
6
|
-
* @param {string} params.seeder Name of single seeder to run (optional)
|
|
7
|
-
*/
|
|
8
|
-
export const runSeeders = async ({seeder}) => {
|
|
9
|
-
// checkout service has no db
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Internal health check (kills process if it fails)
|
|
14
|
-
*/
|
|
15
|
-
export const internalHealthCheck = async () => {
|
|
16
|
-
|
|
17
|
-
// Nothing to check for checkout service
|
|
18
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { logger } from '../../services/logger.service.js';
|
|
2
|
-
import { emailSendingService, emailHeaderLogoUrl } from './../../config.js';
|
|
3
|
-
import { helpers } from '@gnar-engine/helpers';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import handlebars from 'handlebars';
|
|
6
|
-
import nodemailer from 'nodemailer';
|
|
7
|
-
import { getSesClient } from '../../services/ses.service.js';
|
|
8
|
-
import { SendEmailCommand } from '@aws-sdk/client-ses';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Send a notification
|
|
13
|
-
*
|
|
14
|
-
* @param {Object} params
|
|
15
|
-
* @param {string} params.templateName - Name of the template file (without extension)
|
|
16
|
-
* @param {string} params.to - Recipient email address
|
|
17
|
-
* @param {Object} params.params - Parameters to be passed to the template
|
|
18
|
-
* @param {string} params.subject - Subject of the email
|
|
19
|
-
*/
|
|
20
|
-
export const sendNotification = async ({ templateName, to, params, subject }) => {
|
|
21
|
-
let source;
|
|
22
|
-
let template;
|
|
23
|
-
|
|
24
|
-
// get the requested template
|
|
25
|
-
try {
|
|
26
|
-
const workingDir = process.cwd();
|
|
27
|
-
const path = workingDir + '/src/templates/' + templateName + '.hbs';
|
|
28
|
-
source = fs.readFileSync(path, 'utf8');
|
|
29
|
-
} catch (error) {
|
|
30
|
-
logger.error('Error reading template file: ' + error.message);
|
|
31
|
-
throw new Error('Template not found');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// compile the template
|
|
35
|
-
try {
|
|
36
|
-
template = handlebars.compile(source);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
logger.error('Error compiling template: ' + error.message);
|
|
39
|
-
throw new Error('Template compilation failed');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// append other params
|
|
43
|
-
params = prepareParams(params, templateName);
|
|
44
|
-
|
|
45
|
-
// prepare the template
|
|
46
|
-
const html = template(params);
|
|
47
|
-
|
|
48
|
-
// send the email
|
|
49
|
-
switch (emailSendingService) {
|
|
50
|
-
case 'SMTP':
|
|
51
|
-
await sendSmtpEmail({ to, subject, html });
|
|
52
|
-
break;
|
|
53
|
-
|
|
54
|
-
case 'SES':
|
|
55
|
-
await sendSesEmail({ to, subject, html });
|
|
56
|
-
break;
|
|
57
|
-
|
|
58
|
-
case 'Direct':
|
|
59
|
-
logger.error('Email sending service not implemented: ' + emailSendingService);
|
|
60
|
-
throw new Error('Email sending service not implemented');
|
|
61
|
-
|
|
62
|
-
default:
|
|
63
|
-
logger.error('Invalid email sending service: ' + emailSendingService);
|
|
64
|
-
throw new Error('Invalid email sending service');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Send SMTP email
|
|
70
|
-
*
|
|
71
|
-
* @param {Object} params
|
|
72
|
-
* @param {string} params.to - Recipient email address
|
|
73
|
-
* @param {string} params.subject - Email subject
|
|
74
|
-
* @param {string} params.html - HTML content of the email
|
|
75
|
-
*/
|
|
76
|
-
export const sendSmtpEmail = async ({ to, subject, html }) => {
|
|
77
|
-
try {
|
|
78
|
-
const transporter = nodemailer.createTransport({
|
|
79
|
-
host: process.env.SMTP_HOST,
|
|
80
|
-
port: parseInt(process.env.SMTP_PORT || '465'),
|
|
81
|
-
secure: true,
|
|
82
|
-
auth: {
|
|
83
|
-
user: process.env.SMTP_USER,
|
|
84
|
-
pass: process.env.SMTP_PASS
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const mailOptions = {
|
|
89
|
-
from: `"Your App Name" <${process.env.SMTP_USER}>`,
|
|
90
|
-
to,
|
|
91
|
-
subject,
|
|
92
|
-
html
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
await transporter.sendMail(mailOptions);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
logger.error('SMTP email send error: ' + error.message);
|
|
98
|
-
throw new Error('SMTP email failed to send');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Send SES email
|
|
104
|
-
*
|
|
105
|
-
* @param {Object} params
|
|
106
|
-
* @param {string} params.to - Recipient email address
|
|
107
|
-
* @param {string} params.subject - Email subject
|
|
108
|
-
* @param {string} params.html - HTML content of the email
|
|
109
|
-
*/
|
|
110
|
-
export const sendSesEmail = async ({ to, subject, html }) => {
|
|
111
|
-
try {
|
|
112
|
-
const sesClient = getSesClient();
|
|
113
|
-
|
|
114
|
-
const command = new SendEmailCommand({
|
|
115
|
-
Source: process.env.NOTIFICATION_SES_SOURCE_EMAIL,
|
|
116
|
-
Destination: {
|
|
117
|
-
ToAddresses: [to]
|
|
118
|
-
},
|
|
119
|
-
Message: {
|
|
120
|
-
Subject: {
|
|
121
|
-
Data: subject,
|
|
122
|
-
Charset: 'UTF-8'
|
|
123
|
-
},
|
|
124
|
-
Body: {
|
|
125
|
-
Html: {
|
|
126
|
-
Data: html,
|
|
127
|
-
Charset: 'UTF-8'
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
await sesClient.send(command);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
logger.error('Error sending email with SES: ' + error.message);
|
|
136
|
-
throw new Error('SES email failed to send');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Prepare parameters for the template
|
|
142
|
-
*
|
|
143
|
-
* @param {Object} params - Parameters to be passed to the template
|
|
144
|
-
* @param {string} templateName - Name of the template file (without extension)
|
|
145
|
-
* @returns {Object} - Prepared parameters
|
|
146
|
-
*/
|
|
147
|
-
const prepareParams = (params, templateName) => {
|
|
148
|
-
|
|
149
|
-
// add shop logo
|
|
150
|
-
params.logoUrl = emailHeaderLogoUrl;
|
|
151
|
-
|
|
152
|
-
if (params.order?.currency) {
|
|
153
|
-
params.currencySymbol = helpers.ecommerce.getCurrencySymbol(params.order.currency);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return params
|
|
157
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import pino from 'pino';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
6
|
-
|
|
7
|
-
// Create a logger instance
|
|
8
|
-
export const logger = pino({
|
|
9
|
-
level: process.env.LOG_MODE || 'info',
|
|
10
|
-
transport: {
|
|
11
|
-
target: 'pino-pretty', // Pretty print logs for the console
|
|
12
|
-
options: {
|
|
13
|
-
colorize: true, // Colorize the logs in the console
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { SESClient } from '@aws-sdk/client-ses';
|
|
2
|
-
import { logger } from './logger.service.js';
|
|
3
|
-
|
|
4
|
-
let sesClientInstance = null;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Get SES Client Instance
|
|
8
|
-
*
|
|
9
|
-
* @returns {SESClient} - SESClient instance
|
|
10
|
-
* @description Returns a singleton instance of the SESClient
|
|
11
|
-
*/
|
|
12
|
-
export const getSesClient = () => {
|
|
13
|
-
if (!sesClientInstance) {
|
|
14
|
-
sesClientInstance = new SESClient({
|
|
15
|
-
region: process.env.NOTIFICATION_AWS_REGION,
|
|
16
|
-
credentials: {
|
|
17
|
-
accessKeyId: process.env.NOTIFICATION_AWS_ACCESS_KEY_ID,
|
|
18
|
-
secretAccessKey: process.env.NOTIFICATION_AWS_SECRET_ACCESS_KEY
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
return sesClientInstance;
|
|
23
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>New Order Notification</title>
|
|
6
|
-
<style>
|
|
7
|
-
body {
|
|
8
|
-
font-family: Arial, sans-serif;
|
|
9
|
-
font-size: 14px;
|
|
10
|
-
color: #333;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.container {
|
|
14
|
-
max-width: 600px;
|
|
15
|
-
margin: auto;
|
|
16
|
-
padding: 20px;
|
|
17
|
-
border: 1px solid #eee;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.logo {
|
|
21
|
-
text-align: center;
|
|
22
|
-
margin-bottom: 20px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.logo img {
|
|
26
|
-
max-height: 60px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.header {
|
|
30
|
-
text-align: center;
|
|
31
|
-
font-size: 20px;
|
|
32
|
-
margin-bottom: 20px;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
table {
|
|
36
|
-
width: 100%;
|
|
37
|
-
border-collapse: collapse;
|
|
38
|
-
margin-bottom: 20px;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
th, td {
|
|
42
|
-
padding: 10px;
|
|
43
|
-
border: 1px solid #ddd;
|
|
44
|
-
text-align: left;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
th {
|
|
48
|
-
background-color: #f8f8f8;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.totals td {
|
|
52
|
-
font-weight: bold;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.footer {
|
|
56
|
-
font-size: 12px;
|
|
57
|
-
color: #777;
|
|
58
|
-
text-align: center;
|
|
59
|
-
}
|
|
60
|
-
</style>
|
|
61
|
-
</head>
|
|
62
|
-
<body>
|
|
63
|
-
<div class="container">
|
|
64
|
-
<div class="logo">
|
|
65
|
-
<img src="{{ logoUrl }}" alt="Logo">
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div class="header">
|
|
69
|
-
New Order Received
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<p>A new order has been placed by <strong>{{order.billingAddress.firstName}} {{order.billingAddress.lastName}}</strong>.</p>
|
|
73
|
-
|
|
74
|
-
<h3>Customer Contact</h3>
|
|
75
|
-
<p>
|
|
76
|
-
Email: <a href="mailto:{{order.billingAddress.email}}">{{order.billingAddress.email}}</a><br>
|
|
77
|
-
Phone: {{order.billingAddress.phone}}
|
|
78
|
-
</p>
|
|
79
|
-
|
|
80
|
-
<h3>Order Summary</h3>
|
|
81
|
-
<table>
|
|
82
|
-
<thead>
|
|
83
|
-
<tr>
|
|
84
|
-
<th>SKU</th>
|
|
85
|
-
<th>Type</th>
|
|
86
|
-
<th>Price</th>
|
|
87
|
-
<th>Qty</th>
|
|
88
|
-
</tr>
|
|
89
|
-
</thead>
|
|
90
|
-
<tbody>
|
|
91
|
-
{{#each order.lineItems}}
|
|
92
|
-
<tr>
|
|
93
|
-
<td>{{sku}}</td>
|
|
94
|
-
<td>{{type}}</td>
|
|
95
|
-
<td>{{currencySymbol}} {{price.price}}</td>
|
|
96
|
-
<td>{{quantity}}</td>
|
|
97
|
-
</tr>
|
|
98
|
-
{{/each}}
|
|
99
|
-
</tbody>
|
|
100
|
-
</table>
|
|
101
|
-
|
|
102
|
-
<table class="totals">
|
|
103
|
-
<tr>
|
|
104
|
-
<td>Subtotal:</td>
|
|
105
|
-
<td style="text-align: right;">{{currencySymbol}} {{order.subTotal}}</td>
|
|
106
|
-
</tr>
|
|
107
|
-
<tr>
|
|
108
|
-
<td>Tax:</td>
|
|
109
|
-
<td style="text-align: right;">{{currencySymbol}} {{order.tax}}</td>
|
|
110
|
-
</tr>
|
|
111
|
-
<tr>
|
|
112
|
-
<td>Shipping:</td>
|
|
113
|
-
<td style="text-align: right;">{{currencySymbol}} {{order.shipping}}</td>
|
|
114
|
-
</tr>
|
|
115
|
-
<tr>
|
|
116
|
-
<td>Total:</td>
|
|
117
|
-
<td style="text-align: right;"><strong>{{currencySymbol}} {{order.total}}</strong></td>
|
|
118
|
-
</tr>
|
|
119
|
-
</table>
|
|
120
|
-
|
|
121
|
-
<h3>Billing Address</h3>
|
|
122
|
-
<p>
|
|
123
|
-
{{order.billingAddress.firstName}} {{order.billingAddress.lastName}}<br>
|
|
124
|
-
{{order.billingAddress.addressLine1}}<br>
|
|
125
|
-
{{order.billingAddress.addressLine2}}<br>
|
|
126
|
-
{{order.billingAddress.city}}, {{order.billingAddress.county}}<br>
|
|
127
|
-
{{order.billingAddress.postcode}}<br>
|
|
128
|
-
{{order.billingAddress.country}}
|
|
129
|
-
</p>
|
|
130
|
-
|
|
131
|
-
<div class="footer">
|
|
132
|
-
This is an automated notification from GnarEngine.
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</body>
|
|
136
|
-
</html>
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>Subscription Cancelled - Admin Notification</title>
|
|
6
|
-
<style>
|
|
7
|
-
body {
|
|
8
|
-
font-family: Arial, sans-serif;
|
|
9
|
-
font-size: 14px;
|
|
10
|
-
color: #333;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.container {
|
|
14
|
-
max-width: 600px;
|
|
15
|
-
margin: auto;
|
|
16
|
-
padding: 20px;
|
|
17
|
-
border: 1px solid #eee;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.logo {
|
|
21
|
-
text-align: center;
|
|
22
|
-
margin-bottom: 20px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.logo img {
|
|
26
|
-
max-height: 60px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.header {
|
|
30
|
-
text-align: center;
|
|
31
|
-
font-size: 20px;
|
|
32
|
-
margin-bottom: 20px;
|
|
33
|
-
color: #e67e22;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.content p {
|
|
37
|
-
margin-bottom: 15px;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.details {
|
|
41
|
-
border-top: 1px solid #ccc;
|
|
42
|
-
padding-top: 10px;
|
|
43
|
-
margin-top: 20px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.details strong {
|
|
47
|
-
display: inline-block;
|
|
48
|
-
width: 140px;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.footer {
|
|
52
|
-
font-size: 12px;
|
|
53
|
-
color: #777;
|
|
54
|
-
text-align: center;
|
|
55
|
-
margin-top: 30px;
|
|
56
|
-
}
|
|
57
|
-
</style>
|
|
58
|
-
</head>
|
|
59
|
-
<body>
|
|
60
|
-
<div class="container">
|
|
61
|
-
<div class="logo">
|
|
62
|
-
<img src="{{ logoUrl }}" alt="Logo">
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div class="header">
|
|
66
|
-
Subscription Cancelled After Failed Payments
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
<div class="content">
|
|
70
|
-
<p>A customer's subscription has been <strong>cancelled</strong> after three failed payment attempts.</p>
|
|
71
|
-
|
|
72
|
-
<div class="details">
|
|
73
|
-
<p><strong>Customer Name:</strong> {{firstName}} {{lastName}}</p>
|
|
74
|
-
<p><strong>Email:</strong> {{email}}</p>
|
|
75
|
-
<p><strong>Subscription ID:</strong> {{subscriptionId}}</p>
|
|
76
|
-
<p><strong>Reason:</strong> Payment failed 3 times</p>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<p>Please take any follow-up action if required.</p>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<div class="footer">
|
|
83
|
-
This is an automated message from your store notification system.
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</body>
|
|
87
|
-
</html>
|