Skip to content

Commit

Permalink
feat: send emails on creation, completion and cancellation
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcaidev committed Nov 8, 2024
1 parent efedd66 commit 2c279de
Show file tree
Hide file tree
Showing 24 changed files with 320 additions and 63 deletions.
2 changes: 2 additions & 0 deletions services/transaction/database/development/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ create table transaction (
item_name text not null,
item_price numeric not null,
seller_id integer not null,
seller_email text not null,
seller_nickname text,
seller_avatar_url text,
buyer_id integer not null,
buyer_email text not null,
buyer_nickname text,
buyer_avatar_url text,
created_at timestamptz default now() not null,
Expand Down
2 changes: 2 additions & 0 deletions services/transaction/database/production/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ create table transaction (
item_name text not null,
item_price numeric not null,
seller_id integer not null,
seller_email text not null,
seller_nickname text,
seller_avatar_url text,
buyer_id integer not null,
buyer_email text not null,
buyer_nickname text,
buyer_avatar_url text,
created_at timestamptz default now() not null,
Expand Down
14 changes: 8 additions & 6 deletions services/transaction/database/test/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ create table transaction (
item_name text not null,
item_price numeric not null,
seller_id integer not null,
seller_email text not null,
seller_nickname text,
seller_avatar_url text,
buyer_id integer not null,
buyer_email text not null,
buyer_nickname text,
buyer_avatar_url text,
created_at timestamptz default now() not null,
Expand All @@ -15,9 +17,9 @@ create table transaction (
check (completed_at is null or cancelled_at is null)
);

insert into transaction (item_id, item_name, item_price, seller_id, seller_nickname, seller_avatar_url, buyer_id, buyer_nickname, buyer_avatar_url, completed_at, cancelled_at) values
('10f33906-24df-449d-b4fb-fcc6c76606b6', 'item1', 10, 1, 'nickname1', 'https://example.com/avatar1.jpg', 2, 'nickname2', 'https://example.com/avatar2.jpg', null, now()),
('10f33906-24df-449d-b4fb-fcc6c76606b6', 'item1', 10, 1, 'nickname1', 'https://example.com/avatar1.jpg', 3, 'nickname3', 'https://example.com/avatar3.jpg', now(), null),
('2475bad8-43ca-4d23-99ee-224d7d9e6ba7', 'item2', 20, 1, 'nickname1', 'https://example.com/avatar1.jpg', 2, 'nickname2', 'https://example.com/avatar2.jpg', null, null),
('3aed59b0-0fa7-4b8b-b1fd-39b8f46a75b3', 'item3', 30, 1, 'nickname1', 'https://example.com/avatar1.jpg', 3, 'nickname3', 'https://example.com/avatar3.jpg', null, null),
('4469a9e3-28d2-4926-8ffd-3ba3c10119ed', 'item4', 40, 2, 'nickname2', 'https://example.com/avatar2.jpg', 3, 'nickname3', 'https://example.com/avatar3.jpg', null, null);
insert into transaction (item_id, item_name, item_price, seller_id, seller_email, seller_nickname, seller_avatar_url, buyer_id, buyer_email, buyer_nickname, buyer_avatar_url, completed_at, cancelled_at) values
('10f33906-24df-449d-b4fb-fcc6c76606b6', 'item1', 10, 1, '[email protected]', 'nickname1', 'https://example.com/avatar1.jpg', 2, '[email protected]', 'nickname2', 'https://example.com/avatar2.jpg', null, now()),
('10f33906-24df-449d-b4fb-fcc6c76606b6', 'item1', 10, 1, '[email protected]', 'nickname1', 'https://example.com/avatar1.jpg', 3, '[email protected]', 'nickname3', 'https://example.com/avatar3.jpg', now(), null),
('2475bad8-43ca-4d23-99ee-224d7d9e6ba7', 'item2', 20, 1, '[email protected]', 'nickname1', 'https://example.com/avatar1.jpg', 2, '[email protected]', 'nickname2', 'https://example.com/avatar2.jpg', null, null),
('3aed59b0-0fa7-4b8b-b1fd-39b8f46a75b3', 'item3', 30, 1, '[email protected]', 'nickname1', 'https://example.com/avatar1.jpg', 3, '[email protected]', 'nickname3', 'https://example.com/avatar3.jpg', null, null),
('4469a9e3-28d2-4926-8ffd-3ba3c10119ed', 'item4', 40, 2, '[email protected]', 'nickname2', 'https://example.com/avatar2.jpg', 3, '[email protected]', 'nickname3', 'https://example.com/avatar3.jpg', null, null);
14 changes: 14 additions & 0 deletions services/transaction/src/events/consume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ await consumeEvent("delayed", "transaction.auto-completed", async (data) => {
console.log(`Auto completed [Transaction ${transaction.id}]`);

publishEvent("transaction", "transaction.completed", newTransaction);
publishEvent("notification", "batch-email", {
emails: [
{
to: transaction.seller.email,
title: "Your deal has been automatically marked as completed",
content: `Dear ${transaction.seller.nickname ?? transaction.seller.email},\n\nThis is to confirm that your deal, after 14 days of inactivity, has been automatically marked as completed on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${transaction.item.name}\nPrice: ${transaction.item.price}\nBuyer: ${transaction.buyer.nickname ?? transaction.buyer.email}\n\nYou could view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
{
to: transaction.buyer.email,
title: "Your deal has been automatically marked as completed",
content: `Dear ${transaction.buyer.nickname ?? transaction.buyer.email},\n\nThis is to confirm that your deal, after 14 days of inactivity, has been automatically marked as completed on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${transaction.item.name}\nPrice: ${transaction.item.price}\nSeller: ${transaction.seller.nickname ?? transaction.seller.email}\n\nYou could view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
],
});
});

export async function consumeEvent(
Expand Down
1 change: 1 addition & 0 deletions services/transaction/src/events/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const channel = await connection.createChannel();
await channel.assertExchange("account", "topic");
await channel.assertExchange("item", "topic");
await channel.assertExchange("transaction", "topic");
await channel.assertExchange("notification", "topic");
await channel.assertExchange("delayed", "x-delayed-message", {
arguments: { "x-delayed-type": "topic" },
});
1 change: 1 addition & 0 deletions services/transaction/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as v from "valibot";

const payloadSchema = v.object({
id: v.number("jwt.id should be a number"),
email: v.string("jwt.email should be a string"),
nickname: v.optional(
v.nullable(v.string("jwt.nickname should be a string")),
null,
Expand Down
30 changes: 17 additions & 13 deletions services/transaction/src/transactions/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,20 @@ type InsertDto = Pick<Transaction, "item" | "seller" | "buyer">;
export async function insertOne(dto: InsertDto) {
const { rows } = await db.query<DbTransaction>(
`
insert into transaction (item_id, item_name, item_price, seller_id, seller_nickname, seller_avatar_url, buyer_id, buyer_nickname, buyer_avatar_url)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
insert into transaction (item_id, item_name, item_price, seller_id, seller_email, seller_nickname, seller_avatar_url, buyer_id, buyer_email, buyer_nickname, buyer_avatar_url)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
returning *
`,
[
dto.item.id,
dto.item.name,
dto.item.price,
dto.seller.id,
dto.seller.email,
dto.seller.nickname,
dto.seller.avatarUrl,
dto.buyer.id,
dto.buyer.email,
dto.buyer.nickname,
dto.buyer.avatarUrl,
],
Expand Down Expand Up @@ -120,19 +122,19 @@ export async function updateParticipant(partipant: Participant) {
const { rowCount: rowCount1 } = await db.query(
`
update transaction
set seller_nickname = $2, seller_avatar_url = $3
set seller_email = $2, seller_nickname = $3, seller_avatar_url = $4
where seller_id = $1
`,
[partipant.id, partipant.nickname, partipant.avatarUrl],
[partipant.id, partipant.email, partipant.nickname, partipant.avatarUrl],
);

const { rowCount: rowCount2 } = await db.query(
`
update transaction
set buyer_nickname = $2, buyer_avatar_url = $3
set buyer_email = $2, buyer_nickname = $3, buyer_avatar_url = $4
where buyer_id = $1
`,
[partipant.id, partipant.nickname, partipant.avatarUrl],
[partipant.id, partipant.email, partipant.nickname, partipant.avatarUrl],
);

return rowCount1! + rowCount2!;
Expand Down Expand Up @@ -181,26 +183,28 @@ export async function cancelManyByItemId(itemId: string) {
return rowCount!;
}

function rowToTransaction(row: DbTransaction) {
function rowToTransaction(row: DbTransaction): Transaction {
return {
id: row.id,
item: {
id: row.item_id,
name: row.item_name,
price: row.item_price,
},
buyer: {
id: row.buyer_id,
nickname: row.buyer_nickname,
avatarUrl: row.buyer_avatar_url,
},
seller: {
id: row.seller_id,
email: row.seller_email,
nickname: row.seller_nickname,
avatarUrl: row.seller_avatar_url,
},
buyer: {
id: row.buyer_id,
email: row.buyer_email,
nickname: row.buyer_nickname,
avatarUrl: row.buyer_avatar_url,
},
createdAt: row.created_at,
completedAt: row.completed_at,
cancelledAt: row.cancelled_at,
} as Transaction;
};
}
22 changes: 19 additions & 3 deletions services/transaction/src/transactions/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,35 @@ export async function create(dto: CreateDto) {
price: detailedItem.price,
},
seller: {
id: detailedItem.seller.id,
nickname: detailedItem.seller.nickname,
avatarUrl: detailedItem.seller.avatarUrl,
id: dto.user.id,
email: dto.user.email,
nickname: dto.user.nickname,
avatarUrl: dto.user.avatarUrl,
},
buyer: {
id: buyerAccount.id,
email: buyerAccount.email,
nickname: buyerAccount.nickname,
avatarUrl: buyerAccount.avatarUrl,
},
});

publishEvent("transaction", "transaction.created", transaction);
publishEvent("transaction", "transaction.auto-completed", transaction);
publishEvent("notification", "batch-email", {
emails: [
{
to: dto.user.email,
title: "You have made a deal on NUS Second-Hand Market",
content: `Dear ${dto.user.nickname ?? dto.user.email},\n\nThis is to confirm that you have made a deal for your second-hand item on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${detailedItem.name}\nPrice: ${detailedItem.price}\nBuyer: ${buyerAccount.nickname ?? buyerAccount.email}\n\nPlease proceed to contact the buyer to arrange for the transaction offline.\nYou could also view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
{
to: buyerAccount.email,
title: "You have secured a deal on NUS Second-Hand Market",
content: `Dear ${buyerAccount.nickname ?? buyerAccount.email},\n\nThis is to confirm that you have secured a deal for a second-hand item you have listed as wanted on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${detailedItem.name}\nPrice: ${detailedItem.price}\nSeller: ${dto.user.nickname ?? dto.user.email}\n\nPlease proceed to contact the seller to arrange for the transaction offline.\nYou could also view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
],
});

return transaction;
}
Expand Down
29 changes: 29 additions & 0 deletions services/transaction/src/transactions/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ async function complete(transaction: Transaction, actor: Participant) {
);

publishEvent("transaction", "transaction.completed", newTransaction);
publishEvent("notification", "batch-email", {
emails: [
{
to: transaction.seller.email,
title: "Buyer has confirmed the receipt of your second-hand item",
content: `Dear ${transaction.seller.nickname ?? transaction.seller.email},\n\nThis is to confirm that the buyer of your second-hand item has confirmed the receipt on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${transaction.item.name}\nPrice: ${transaction.item.price}\nBuyer: ${transaction.buyer.nickname ?? transaction.buyer.email}\n\nThis transaction has now been marked as completed.\nYou could also view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
{
to: transaction.buyer.email,
title: "You have confirmed the receipt of a second-hand item",
content: `Dear ${transaction.buyer.nickname ?? transaction.buyer.email},\n\nThis is to confirm that you have confirmed the receipt of a second-hand item you have purchased on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${transaction.item.name}\nPrice: ${transaction.item.price}\nSeller: ${transaction.seller.nickname ?? transaction.seller.email}\n\nThis transaction has now been marked as completed.\nYou could also view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
],
});
}

async function cancel(transaction: Transaction, actor: Participant) {
Expand All @@ -62,4 +76,19 @@ async function cancel(transaction: Transaction, actor: Participant) {
);

publishEvent("transaction", "transaction.cancelled", newTransaction);
publishEvent("notification", "batch-email", {
emails: [
{
to: transaction.seller.email,
title: "You have cancelled a deal on NUS Second-Hand Market",
content: `Dear ${transaction.seller.nickname ?? transaction.seller.email},\n\nThis is to confirm that you have cancelled a deal on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${transaction.item.name}\nPrice: ${transaction.item.price}\nBuyer: ${transaction.buyer.nickname ?? transaction.buyer.email}\n\nThe transaction has been aborted, and you could <a href="https://www.nshm.store/items/${transaction.item.id}">make a new deal</a> with someone else.\nYou could also view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
{
to: transaction.buyer.email,
title:
"The seller has cancelled the deal with you on NUS Second-Hand Market",
content: `Dear ${transaction.buyer.nickname ?? transaction.buyer.email},\n\nThis is to confirm that the seller has cancelled the deal with you on NUS Second-Hand Market.\n\nTransaction Details:\nItem: ${transaction.item.name}\nPrice: ${transaction.item.price}\nSeller: ${transaction.seller.nickname ?? transaction.seller.email}\n\nThe transaction has been aborted.\nYou could also view your <a href="https://www.nshm.store/transactions">transaction history</a> online at any time.\n\nBest Regards,\nNUS Second-Hand Market`,
},
],
});
}
10 changes: 6 additions & 4 deletions services/transaction/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export type Participant = {
id: number;
email: string;
nickname: string | null;
avatarUrl: string | null;
};

export type Account = Participant & {
email: string;
department: {
id: number;
acronym: string;
Expand Down Expand Up @@ -40,12 +40,14 @@ export type DbTransaction = {
item_id: string;
item_name: string;
item_price: number;
buyer_id: number;
buyer_nickname: string | null;
buyer_avatar_url: string | null;
seller_id: number;
seller_email: string;
seller_nickname: string | null;
seller_avatar_url: string | null;
buyer_id: number;
buyer_email: string;
buyer_nickname: string | null;
buyer_avatar_url: string | null;
created_at: string;
completed_at: string | null;
cancelled_at: string | null;
Expand Down
6 changes: 4 additions & 2 deletions services/transaction/tests/test-utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { sign } from "jsonwebtoken";

export const participant1: Participant = {
id: 1,
email: "[email protected]",
nickname: "nickname1",
avatarUrl: "https://example.com/avatar1.jpg",
};

export const account1: Account = {
...participant1,
email: "[email protected]",
phoneCode: "65",
phoneNumber: "12345678",
department: {
Expand All @@ -25,6 +25,7 @@ export const account1: Account = {
export const jwt1 = sign(
{
id: account1.id,
email: account1.email,
nickname: account1.nickname,
avatar_url: account1.avatarUrl,
},
Expand All @@ -33,13 +34,13 @@ export const jwt1 = sign(

export const participant2: Participant = {
id: 2,
email: "[email protected]",
nickname: "nickname2",
avatarUrl: "https://example.com/avatar2.jpg",
};

export const account2: Account = {
...participant2,
email: "[email protected]",
phoneCode: "65",
phoneNumber: "22345678",
department: {
Expand All @@ -55,6 +56,7 @@ export const account2: Account = {
export const jwt2 = sign(
{
id: account2.id,
email: account2.email,
nickname: account2.nickname,
avatar_url: account2.avatarUrl,
},
Expand Down
Loading

0 comments on commit 2c279de

Please sign in to comment.