Skip to content

Commit 95fe042

Browse files
committed
Make email confirmation functionality as function and store purchase data test rework
1 parent 7864588 commit 95fe042

4 files changed

Lines changed: 320 additions & 72 deletions

File tree

astro/src/lib/email.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export interface EmailOptions {
2+
email: string;
3+
purchaseCode: string;
4+
organization: string;
5+
theme: string;
6+
}
7+
8+
export async function sendConfirmationEmail(options: EmailOptions): Promise<void> {
9+
const { email, purchaseCode, organization, theme } = options;
10+
11+
// In a production environment, you would use a service like:
12+
// - SendGrid
13+
// - Nodemailer with SMTP
14+
// - AWS SES
15+
// - Mailgun
16+
// etc.
17+
18+
// For now, we'll just log the email content
19+
const emailContent = `
20+
Subject: Your Accessible Escape Room Kit Purchase Confirmation
21+
22+
Dear ${organization},
23+
24+
Thank you for purchasing an Accessible Escape Room Kit!
25+
26+
Your Purchase Details:
27+
- Kit Type: Build-your-own Kit
28+
- Theme: ${theme}
29+
- Organization: ${organization}
30+
31+
Your Secure Access Code: ${purchaseCode}
32+
33+
To access your kit materials, visit:
34+
https://accessiblecommunity.org/services/escape-room/access
35+
36+
Enter your access code and the email address used for this purchase.
37+
38+
Keep this code secure - it's your proof of purchase and gateway to your materials.
39+
40+
If you need any assistance, please contact us at escaperoom@accessiblecommunity.org
41+
42+
Best regards,
43+
The Accessible Community Team
44+
`;
45+
46+
// In production, replace this with actual email sending
47+
// await sendEmail({
48+
// to: email,
49+
// subject: 'Your Accessible Escape Room Kit Purchase Confirmation',
50+
// text: emailContent,
51+
// html: generateHtmlEmail(purchaseCode, organization, theme)
52+
// });
53+
54+
console.log('Confirmation email prepared for:', email);
55+
console.log(emailContent);
56+
}

astro/src/lib/purchase-storage.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
4+
export interface PurchaseData {
5+
sessionId: string;
6+
purchaseCode: string;
7+
kitType?: string;
8+
theme: string;
9+
organization: string;
10+
contactName: string;
11+
email: string;
12+
specialRequirements?: string;
13+
amountPaid?: number | null;
14+
currency?: string | null;
15+
paymentStatus?: string | null;
16+
createdAt: string;
17+
}
18+
19+
export async function storePurchaseData(purchaseData: PurchaseData): Promise<void> {
20+
try {
21+
// TODO: For user accounts/login system, also store purchase by email:
22+
// - Link purchase codes to customer email addresses
23+
// - Store in database: { email, purchaseCode, productId, purchaseDate }
24+
// - Enable "view all my purchases" functionality
25+
26+
// TODO?: For Google OAuth integration:
27+
// - Add optional account linking after purchase
28+
// - Store OAuth user ID with purchase data
29+
// - Allow login to see purchase history
30+
31+
// Create local storage directory if it doesn't exist
32+
const storageDir = path.join(process.cwd(), '..', 'local-dev', 'purchases');
33+
await fs.mkdir(storageDir, { recursive: true });
34+
35+
// Store purchase data as JSON file
36+
const filename = `${purchaseData.purchaseCode}.json`;
37+
const filepath = path.join(storageDir, filename);
38+
39+
await fs.writeFile(filepath, JSON.stringify(purchaseData, null, 2));
40+
// Purchase data stored successfully
41+
} catch (error) {
42+
console.error('Error storing purchase data:', error);
43+
throw error;
44+
}
45+
}
46+
47+
function getCandidatePurchaseDirs(): string[] {
48+
const env = import.meta.env as Record<string, string | undefined>;
49+
const candidates = [
50+
env.PURCHASES_DATA_DIR,
51+
process.env.PURCHASES_DATA_DIR,
52+
path.resolve(process.cwd(), '..', 'local-dev', 'purchases'),
53+
path.resolve(process.cwd(), 'local-dev', 'purchases')
54+
];
55+
return [...new Set(candidates.filter(Boolean) as string[])];
56+
}
57+
58+
export async function getPurchaseData(purchaseCode: string): Promise<PurchaseData | null> {
59+
const candidates = getCandidatePurchaseDirs();
60+
const filename = `${purchaseCode}.json`;
61+
62+
for (const dir of candidates) {
63+
const filepath = path.join(dir, filename);
64+
try {
65+
const data = await fs.readFile(filepath, 'utf-8');
66+
return JSON.parse(data);
67+
} catch (error: any) {
68+
if (error?.code === 'ENOENT') {
69+
continue;
70+
}
71+
console.error('Error reading purchase data:', { filepath, error });
72+
throw error;
73+
}
74+
}
75+
76+
return null;
77+
}

astro/src/pages/api/send-confirmation-email.ts

Lines changed: 0 additions & 72 deletions
This file was deleted.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import fs from 'fs/promises';
3+
import { storePurchaseData, getPurchaseData } from '@lib/purchase-storage';
4+
import type { PurchaseData } from '@lib/purchase-storage';
5+
6+
// Mock fs module
7+
vi.mock('fs/promises', () => ({
8+
default: {
9+
mkdir: vi.fn(),
10+
writeFile: vi.fn(),
11+
readFile: vi.fn(),
12+
},
13+
}));
14+
15+
describe('purchase-storage', () => {
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
19+
// Mock successful file operations
20+
(fs.mkdir as any).mockResolvedValue(undefined);
21+
(fs.writeFile as any).mockResolvedValue(undefined);
22+
});
23+
24+
afterEach(() => {
25+
vi.restoreAllMocks();
26+
});
27+
28+
describe('storePurchaseData', () => {
29+
it('should store purchase data to JSON file', async () => {
30+
const purchaseData: PurchaseData = {
31+
sessionId: 'cs_test_session123',
32+
purchaseCode: 'ESC-12345678',
33+
theme: 'corporate',
34+
organization: 'Test Corp',
35+
contactName: 'John Doe',
36+
email: 'test@example.com',
37+
createdAt: new Date().toISOString(),
38+
};
39+
40+
await storePurchaseData(purchaseData);
41+
42+
expect(fs.mkdir).toHaveBeenCalledWith(
43+
expect.stringMatching(/local-dev[\/\\]purchases$/),
44+
{ recursive: true }
45+
);
46+
47+
expect(fs.writeFile).toHaveBeenCalledWith(
48+
expect.stringMatching(/ESC-12345678\.json$/),
49+
expect.stringContaining('"purchaseCode":"ESC-12345678"')
50+
);
51+
});
52+
53+
it('should store complete purchase data with all fields', async () => {
54+
const purchaseData: PurchaseData = {
55+
sessionId: 'cs_test_session123',
56+
purchaseCode: 'ESC-87654321',
57+
kitType: 'build',
58+
theme: 'kitchen',
59+
organization: 'Bakery Inc',
60+
contactName: 'Jane Baker',
61+
email: 'jane@bakery.com',
62+
specialRequirements: 'Gluten-free options',
63+
amountPaid: 50000,
64+
currency: 'usd',
65+
paymentStatus: 'paid',
66+
createdAt: new Date().toISOString(),
67+
};
68+
69+
await storePurchaseData(purchaseData);
70+
71+
const writeCall = (fs.writeFile as any).mock.calls[0];
72+
const savedData = JSON.parse(writeCall[1]);
73+
74+
expect(savedData).toMatchObject({
75+
sessionId: 'cs_test_session123',
76+
purchaseCode: 'ESC-87654321',
77+
kitType: 'build',
78+
theme: 'kitchen',
79+
organization: 'Bakery Inc',
80+
contactName: 'Jane Baker',
81+
email: 'jane@bakery.com',
82+
specialRequirements: 'Gluten-free options',
83+
amountPaid: 50000,
84+
currency: 'usd',
85+
paymentStatus: 'paid',
86+
});
87+
});
88+
89+
it('should handle file system errors', async () => {
90+
(fs.mkdir as any).mockRejectedValue(new Error('Permission denied'));
91+
92+
const purchaseData: PurchaseData = {
93+
sessionId: 'cs_test_session123',
94+
purchaseCode: 'ESC-12345678',
95+
theme: 'corporate',
96+
organization: 'Test Corp',
97+
contactName: 'John Doe',
98+
email: 'test@example.com',
99+
createdAt: new Date().toISOString(),
100+
};
101+
102+
await expect(storePurchaseData(purchaseData)).rejects.toThrow('Permission denied');
103+
});
104+
105+
it('should create directory if it does not exist', async () => {
106+
const purchaseData: PurchaseData = {
107+
sessionId: 'cs_test_session123',
108+
purchaseCode: 'ESC-12345678',
109+
theme: 'corporate',
110+
organization: 'Test Corp',
111+
contactName: 'John Doe',
112+
email: 'test@example.com',
113+
createdAt: new Date().toISOString(),
114+
};
115+
116+
await storePurchaseData(purchaseData);
117+
118+
expect(fs.mkdir).toHaveBeenCalledWith(
119+
expect.any(String),
120+
{ recursive: true }
121+
);
122+
});
123+
});
124+
125+
describe('getPurchaseData', () => {
126+
it('should retrieve purchase data by code', async () => {
127+
const mockData: PurchaseData = {
128+
sessionId: 'cs_test_session123',
129+
purchaseCode: 'ESC-12345678',
130+
theme: 'corporate',
131+
organization: 'Test Corp',
132+
contactName: 'John Doe',
133+
email: 'test@example.com',
134+
createdAt: '2025-11-20T00:00:00.000Z',
135+
};
136+
137+
(fs.readFile as any).mockResolvedValue(JSON.stringify(mockData));
138+
139+
const result = await getPurchaseData('ESC-12345678');
140+
141+
expect(result).toEqual(mockData);
142+
expect(fs.readFile).toHaveBeenCalledWith(
143+
expect.stringMatching(/ESC-12345678\.json$/),
144+
'utf-8'
145+
);
146+
});
147+
148+
it('should return null for non-existent purchase code', async () => {
149+
const error: NodeJS.ErrnoException = new Error('File not found');
150+
error.code = 'ENOENT';
151+
(fs.readFile as any).mockRejectedValue(error);
152+
153+
const result = await getPurchaseData('ESC-NOTFOUND');
154+
155+
expect(result).toBeNull();
156+
});
157+
158+
it('should throw error for other file system errors', async () => {
159+
(fs.readFile as any).mockRejectedValue(new Error('Permission denied'));
160+
161+
await expect(getPurchaseData('ESC-12345678')).rejects.toThrow('Permission denied');
162+
});
163+
164+
it('should parse JSON data correctly', async () => {
165+
const mockData: PurchaseData = {
166+
sessionId: 'cs_test_session123',
167+
purchaseCode: 'ESC-99999999',
168+
kitType: 'build',
169+
theme: 'kitchen',
170+
organization: 'Test Org',
171+
contactName: 'Test User',
172+
email: 'test@test.com',
173+
specialRequirements: 'Test requirements',
174+
amountPaid: 50000,
175+
currency: 'usd',
176+
paymentStatus: 'paid',
177+
createdAt: '2025-11-20T00:00:00.000Z',
178+
};
179+
180+
(fs.readFile as any).mockResolvedValue(JSON.stringify(mockData, null, 2));
181+
182+
const result = await getPurchaseData('ESC-99999999');
183+
184+
expect(result).toEqual(mockData);
185+
});
186+
});
187+
});

0 commit comments

Comments
 (0)