Desishub Lessons
Node Js and Typescript
Prisma Transactions

Creating a Prisma Transaction

import prisma from "@/lib/db";
import { generateOrderNumber } from "@/lib/generateOrderNumber";
import { ILineOrder } from "@/types/types";
import { NotificationStatus } from "@prisma/client";
import { revalidatePath } from "next/cache";
 
interface OrderLineItem {
  id: string;
  name: string;
  price: number;
  qty: number;
  productThumbnail: string;
}
 
interface CustomerData {
  customerId: string;
  customerName: string;
  customerEmail: string;
  firstName?: string;
  lastName?: string;
  email?: string;
  phone?: string;
  streetAddress?: string;
  apartment?: string;
  city?: string;
  state?: string;
  zipCode?: string;
  country?: string;
  method?: string;
}
interface NewOrderProps {
  orderItems: OrderLineItem[];
  orderAmount: number;
  orderType: string;
  source: string;
}
type NotificationProps = {
  message: string;
  status?: NotificationStatus;
  statusText: string;
};
export async function createNotification(data: NotificationProps) {
  try {
    const newNot = await prisma.notification.create({
      data,
    });
    revalidatePath("/dashboard");
    return newNot;
  } catch (error) {
    console.log(error);
  }
}
export async function updateNotificationStatusById(id: string) {
  try {
    const updatedNot = await prisma.notification.update({
      where: {
        id,
      },
      data: {
        read: true,
      },
    });
    revalidatePath("/dashboard");
    return updatedNot;
  } catch (error) {
    console.log(error);
  }
}
export async function getNotifications() {
  try {
    const notifications = await prisma.notification.findMany({
      orderBy: {
        createdAt: "desc",
      },
      where: {
        read: false,
      },
    });
    return notifications;
  } catch (error) {
    console.log(error);
  }
}
export async function createLineOrder(
  newOrder: NewOrderProps,
  customerData: CustomerData
) {
  const { orderItems, orderAmount, orderType, source } = newOrder;
  try {
    const lineOrderId = await prisma.$transaction(async (transaction) => {
      // Create the Line Order
      const lineOrder = await transaction.lineOrder.create({
        data: {
          customerId: customerData.customerId,
          customerName: customerData.customerName,
          customerEmail: customerData.customerEmail,
          // Personal Details
          firstName: customerData.firstName,
          lastName: customerData.lastName,
          phone: customerData.phone,
          email: customerData.email,
          // Shipping address
          streetAddress: customerData.streetAddress,
          apartment: customerData.apartment,
          city: customerData.city,
          state: customerData.state,
          zipCode: customerData.zipCode,
          country: customerData.country,
          paymentMethod: customerData.method,
          // payment Method
          orderNumber: generateOrderNumber(),
          orderAmount,
          orderType,
          source,
          status: source === "pos" ? "DELIVERED" : "PROCESSING",
        },
      });
 
      for (const item of orderItems) {
        // Update Product stock quantity
        const updatedProduct = await transaction.product.update({
          where: { id: item.id },
          data: {
            stockQty: {
              decrement: item.qty,
            },
          },
        });
 
        if (!updatedProduct) {
          throw new Error(`Failed to update stock for product ID: ${item.id}`);
        }
 
        if (updatedProduct.stockQty < updatedProduct.alertQty) {
          // Send/Create the Notification
          const message =
            updatedProduct.stockQty === 0
              ? `The stock of ${updatedProduct.name} is out. Current stock: ${updatedProduct.stockQty}.`
              : `The stock of ${updatedProduct.name} has gone below threshold. Current stock: ${updatedProduct.stockQty}.`;
          const statusText =
            updatedProduct.stockQty === 0 ? "Stock Out" : "Warning";
          const status: NotificationStatus =
            updatedProduct.stockQty === 0 ? "DANGER" : "WARNING";
 
          const newNotification = {
            message,
            status,
            statusText,
          };
          await createNotification(newNotification);
          // Send email
        }
        // Create Line Order Item
        const lineOrderItem = await transaction.lineOrderItem.create({
          data: {
            orderId: lineOrder.id,
            productId: item.id,
            name: item.name,
            price: item.price,
            qty: item.qty,
            productThumbnail: item.productThumbnail,
          },
        });
 
        if (!lineOrderItem) {
          throw new Error(
            `Failed to create line order item for product ID: ${item.id}`
          );
        }
 
        // Create Sale
        const sale = await transaction.sale.create({
          data: {
            orderId: lineOrder.id,
            productId: item.id,
            qty: item.qty,
            salePrice: item.price,
            productName: item.name,
            productImage: item.productThumbnail,
            customerName: customerData.customerName,
            customerEmail: customerData.customerEmail,
          },
        });
 
        if (!sale) {
          throw new Error(`Failed to create sale for product ID: ${item.id}`);
        }
      }
      // console.log(savedLineOrder);
      revalidatePath("/dashboard/sales");
      return lineOrder.id;
    });
 
    const savedLineOrder = await prisma.lineOrder.findUnique({
      where: {
        id: lineOrderId,
      },
      include: {
        lineOrderItems: true,
      },
    });
    // console.log(savedLineOrder);
    return savedLineOrder as ILineOrder;
  } catch (error) {
    console.error("Transaction error:", error);
    throw error; // Propagate the error to the caller
  }
}

Relationship between Sales and Shops

The relationship between sale and shop is a one-to-many , sales must belong to aprticular shop Modifiy the Models

model Shop {
  id            String    @id @default(auto()) @map("_id") @db.ObjectId
  name          String
  slug          String   @unique
  location      String
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
  adminId       String   @db.ObjectId
  products      Product[]
  admin         User     @relation(fields: [adminId], references: [id])
  attendantIds  String[] @db.ObjectId
  sales         Sale[]   // Add this relation
}
 
model Sale {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  customerId      String    @db.ObjectId
  customerName    String?
  saleNumber      String    @unique
  customerEmail   String?
  saleAmount      Int
  balanceAmount   Int?     @default(0)
  paidAmount      Int
  saleType        SaleType @default(PAID)
  paymentMethod   PaymentMethod @default(CASH)
  transactionCode String?
  shopId          String    @db.ObjectId // Add this field
  // Relationships
  saleItems       SaleItem[]
  customer        Customer   @relation(fields: [customerId], references: [id])
  shop            Shop       @relation(fields: [shopId], references: [id]) // Add this relation
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}
 
 

Sales Analytics Controllers

1. getShopSales Controller This controller will return sales for a specific shop, categorized by the periods: today, this week, this month, and all time.

import { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
import {
  startOfDay,
  startOfWeek,
  startOfMonth,
  endOfDay,
  endOfWeek,
  endOfMonth,
} from "date-fns";
 
const prisma = new PrismaClient();
 
export async function getShopSales(req: Request, res: Response) {
  const { shopId } = req.params;
 
  // Define time periods
  const todayStart = startOfDay(new Date());
  const todayEnd = endOfDay(new Date());
  const weekStart = startOfWeek(new Date());
  const weekEnd = endOfWeek(new Date());
  const monthStart = startOfMonth(new Date());
  const monthEnd = endOfMonth(new Date());
 
  try {
    // Fetch sales for different periods
    const salesToday = await prisma.sale.findMany({
      where: {
        shopId,
        createdAt: {
          gte: todayStart,
          lte: todayEnd,
        },
      },
    });
 
    const salesThisWeek = await prisma.sale.findMany({
      where: {
        shopId,
        createdAt: {
          gte: weekStart,
          lte: weekEnd,
        },
      },
    });
 
    const salesThisMonth = await prisma.sale.findMany({
      where: {
        shopId,
        createdAt: {
          gte: monthStart,
          lte: monthEnd,
        },
      },
    });
 
    const salesAllTime = await prisma.sale.findMany({
      where: {
        shopId,
      },
    });
 
    res.status(200).json({
      today: salesToday,
      thisWeek: salesThisWeek,
      thisMonth: salesThisMonth,
      allTime: salesAllTime,
      error: null,
    });
  } catch (error) {
    console.log(error);
    return res.status(500).json({
      error: "Something went wrong",
      data: null,
    });
  }
}

2. getShopsSales Controller This controller will return sales data for all existing shops, categorized by the same periods: today, this week, this month, and all time.

import { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
import {
  startOfDay,
  startOfWeek,
  startOfMonth,
  endOfDay,
  endOfWeek,
  endOfMonth,
} from "date-fns";
 
const prisma = new PrismaClient();
 
export async function getShopsSales(req: Request, res: Response) {
  // Define time periods
  const todayStart = startOfDay(new Date());
  const todayEnd = endOfDay(new Date());
  const weekStart = startOfWeek(new Date());
  const weekEnd = endOfWeek(new Date());
  const monthStart = startOfMonth(new Date());
  const monthEnd = endOfMonth(new Date());
 
  try {
    // Fetch all shops
    const shops = await prisma.shop.findMany();
 
    // Fetch sales for all shops for different periods
    const salesToday = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
      where: {
        createdAt: {
          gte: todayStart,
          lte: todayEnd,
        },
      },
    });
 
    const salesThisWeek = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
      where: {
        createdAt: {
          gte: weekStart,
          lte: weekEnd,
        },
      },
    });
 
    const salesThisMonth = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
      where: {
        createdAt: {
          gte: monthStart,
          lte: monthEnd,
        },
      },
    });
 
    const salesAllTime = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
    });
 
    res.status(200).json({
      today: salesToday,
      thisWeek: salesThisWeek,
      thisMonth: salesThisMonth,
      allTime: salesAllTime,
      error: null,
    });
  } catch (error) {
    console.log(error);
    return res.status(500).json({
      error: "Something went wrong",
      data: null,
    });
  }
}

3. getCategorizedShopSales Controller This controller will return sales for a particular shop, with detailed categorization: sales paid in cash, sales paid by credit, sales paid by mobile money, and sales paid by hand cash.

import { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
import {
  startOfDay,
  startOfWeek,
  startOfMonth,
  endOfDay,
  endOfWeek,
  endOfMonth,
} from "date-fns";
 
const prisma = new PrismaClient();
 
export async function getShopSales(req: Request, res: Response) {
  const { shopId } = req.params;
 
  // Define time periods
  const todayStart = startOfDay(new Date());
  const todayEnd = endOfDay(new Date());
  const weekStart = startOfWeek(new Date());
  const weekEnd = endOfWeek(new Date());
  const monthStart = startOfMonth(new Date());
  const monthEnd = endOfMonth(new Date());
 
  try {
    // Fetch sales for different periods
    const categorizeSales = async (sales: any[]) => {
      return {
        salesPaidInCash: sales.filter(
          (sale) => sale.paymentMethod === "CASH" && sale.balanceAmount <= 0
        ),
        salesPaidInCredit: sales.filter(
          (sale) => sale.paymentMethod === "CASH" && sale.balanceAmount > 0
        ),
        salesByMobileMoney: sales.filter(
          (sale) => sale.paymentMethod === "MOBILE MONEY"
        ),
        salesByHandCash: sales.filter(
          (sale) => sale.paymentMethod === "CASH" && sale.balanceAmount <= 0
        ),
      };
    };
 
    const salesToday = await prisma.sale.findMany({
      where: {
        shopId,
        createdAt: {
          gte: todayStart,
          lte: todayEnd,
        },
      },
    });
 
    const salesThisWeek = await prisma.sale.findMany({
      where: {
        shopId,
        createdAt: {
          gte: weekStart,
          lte: weekEnd,
        },
      },
    });
 
    const salesThisMonth = await prisma.sale.findMany({
      where: {
        shopId,
        createdAt: {
          gte: monthStart,
          lte: monthEnd,
        },
      },
    });
 
    const salesAllTime = await prisma.sale.findMany({
      where: {
        shopId,
      },
    });
 
    res.status(200).json({
      today: await categorizeSales(salesToday),
      thisWeek: await categorizeSales(salesThisWeek),
      thisMonth: await categorizeSales(salesThisMonth),
      allTime: await categorizeSales(salesAllTime),
      error: null,
    });
  } catch (error) {
    console.log(error);
    return res.status(500).json({
      error: "Something went wrong",
      data: null,
    });
  }
}

4. getCategorizedShopsSales Controller This controller will return sales for all shops, categorized similarly: sales paid in cash, sales paid by credit, sales by mobile money, and sales by hand cash.

import { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
import {
  startOfDay,
  startOfWeek,
  startOfMonth,
  endOfDay,
  endOfWeek,
  endOfMonth,
} from "date-fns";
 
const prisma = new PrismaClient();
 
export async function getShopsSales(req: Request, res: Response) {
  // Define time periods
  const todayStart = startOfDay(new Date());
  const todayEnd = endOfDay(new Date());
  const weekStart = startOfWeek(new Date());
  const weekEnd = endOfWeek(new Date());
  const monthStart = startOfMonth(new Date());
  const monthEnd = endOfMonth(new Date());
 
  try {
    // Fetch all sales and group by shopId for different periods
    const categorizeSales = (sales: any[]) => {
      return {
        salesPaidInCash: sales.filter(
          (sale) => sale.paymentMethod === "CASH" && sale.balanceAmount <= 0
        ),
        salesPaidInCredit: sales.filter(
          (sale) => sale.paymentMethod === "CASH" && sale.balanceAmount > 0
        ),
        salesByMobileMoney: sales.filter(
          (sale) => sale.paymentMethod === "MOBILE MONEY"
        ),
        salesByHandCash: sales.filter(
          (sale) => sale.paymentMethod === "CASH" && sale.balanceAmount <= 0
        ),
      };
    };
 
    const salesToday = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
      where: {
        createdAt: {
          gte: todayStart,
          lte: todayEnd,
        },
      },
    });
 
    const salesThisWeek = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
      where: {
        createdAt: {
          gte: weekStart,
          lte: weekEnd,
        },
      },
    });
 
    const salesThisMonth = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
      where: {
        createdAt: {
          gte: monthStart,
          lte: monthEnd,
        },
      },
    });
 
    const salesAllTime = await prisma.sale.groupBy({
      by: ["shopId"],
      _sum: {
        saleAmount: true,
      },
    });
 
    res.status(200).json({
      today: categorizeSales(salesToday),
      thisWeek: categorizeSales(salesThisWeek),
      thisMonth: categorizeSales(salesThisMonth),
      allTime: categorizeSales(salesAllTime),
      error: null,
    });
  } catch (error) {
    console.log(error);
    return res.status(500).json({
      error: "Something went wrong",
      data: null,
    });
  }
}

Explanation: categorizeSales Function:

salesPaidInCash: Sales where the payment method is cash and the balance amount is 0. salesPaidInCredit: Sales where the payment method is cash but the balance amount is greater than 0. salesByMobileMoney: Sales where the payment method is mobile money. salesByHandCash: Sales where the payment method is cash and the balance amount is 0 (same as salesPaidInCash). groupBy Method:

Used in getShopsSales to aggregate sales by shopId and calculate sums of saleAmount.