Source: service/public-service.js

import { ResponseError } from "../error/response-error.js";
import { prismaClient } from "../app/database.js";
import bcrypt from "bcrypt";
import { Role } from "@prisma/client";
import { validate } from "../validation/validation.js";
import {
  loginUserValidation,
  registrationUserValidation,
} from "../validation/user-validation.js";
import { generateJWT } from "../helpers/jwt-config.js";
import { capitalizeWord } from "../helpers/string-helper.js";
import {
  loanValidation,
  returnValidation,
} from "../validation/loan-validation.js";
import { tr } from "@faker-js/faker";

/**
 * Registrasi pengguna baru
 *
 * Fungsi untuk melakukan registrasi akun pengguna baru. Mengecek apakah email yang dimasukkan sudah ada.
 * Jika sudah ada, maka akan mengembalikan pesan kesalahan. Jika tidak, password akan di-hash dan akun pengguna
 * baru akan dibuat dengan data yang diterima dari body request.
 *
 * @async
 * @function registrasi
 * @param {object} req - Request objek yang berisi data pengguna yang akan didaftarkan
 * @param {object} res - Response objek yang akan mengirimkan hasil operasi ke client
 * @returns {Promise<object>} Data pengguna yang baru saja terdaftar
 * @throws {ResponseError} 400 - Jika email sudah terdaftar
 */
const registrasi = async (req, res) => {
  const data = validate(registrationUserValidation, req.body);

  const checkAccountDuplicate = await prismaClient.student.findUnique({
    where: { email: data.email },
  });

  if (checkAccountDuplicate) {
    throw new ResponseError(400, "email already exists!");
  }

  data.password = await bcrypt.hash(data.password, 10);
  // Capital nama
  data.name = capitalizeWord(data.name);

  const result = await prismaClient.student.create({
    data: {
      name: data.name,
      email: data.email,
      password: data.password,
      role: Role.Student,
      gender: data.gender,
    },
  });

  return prismaClient.student.findFirst({
    where: { id: result.id },
  });
};

/**
 * Login pengguna
 *
 * Fungsi untuk memverifikasi email dan password pengguna. Jika data yang dimasukkan salah,
 * maka akan mengembalikan pesan kesalahan. Jika berhasil, akan mengembalikan token JWT untuk otentikasi lebih lanjut.
 *
 * @async
 * @function login
 * @param {object} req - Request objek yang berisi data login pengguna
 * @param {object} res - Response objek yang akan mengirimkan hasil operasi ke client
 * @returns {Promise<string>} Token JWT untuk otentikasi pengguna
 * @throws {ResponseError} 400 - Jika email atau password salah
 */
const login = async (req, res) => {
  const data = validate(loginUserValidation, req.body);

  const account = await prismaClient.student.findFirst({
    where: { email: data.email },
    select: { name: true, email: true, password: true, role: true },
  });

  if (!account) {
    throw new ResponseError(400, "Email or password is wrong!");
  }

  const isPasswordSame = await bcrypt.compare(data.password, account.password);

  if (!isPasswordSame) {
    throw new ResponseError(400, "Email or password is wrong!");
  }

  return generateJWT(account.name, account.role);
};

/**
 * Mendapatkan daftar buku
 *
 * Fungsi untuk mengambil semua daftar buku yang tersedia. Buku yang sudah dihapus akan diabaikan,
 * dan informasi terkait genre serta penerbit buku akan ditambahkan.
 *
 * @async
 * @function getBookList
 * @param {object} req - Request objek yang berisi permintaan daftar buku
 * @param {object} res - Response objek yang akan mengirimkan hasil operasi ke client
 * @returns {Promise<object[]>} Daftar buku yang tersedia
 */
const getBookList = async (req, res) => {
  const data = await prismaClient.book.findMany({
    select: {
      id: true,
      title: true,
      author: true,
      description: true,
      isbn: true,
      stok: true,
      deteled_at: true,
      genres: {
        select: {
          genre: {
            select: {
              name: true,
            },
          },
        },
      },
      publisher: {
        select: {
          name: true,
        },
      },
    },
  });

  return data
    .filter(book => book.deteled_at === null)
    .map(book => ({
      id: book.id,
      title: book.title,
      author: book.author,
      description: book.description,
      isbn: book.isbn,
      stok: book.stok,
      genres: book.genres.map(genre => genre.genre.name),
      publisher: book.publisher?.name || "Unknown",
    }));
};

/**
 * Mendapatkan detail buku berdasarkan ID
 *
 * Fungsi untuk mengambil data detail buku berdasarkan ID. Jika buku tidak ditemukan atau sudah dihapus,
 * maka akan mengembalikan pesan kesalahan.
 *
 * @async
 * @function getBookById
 * @param {string} id - ID buku yang akan dicari
 * @returns {Promise<object>} Detail buku yang ditemukan
 * @throws {ResponseError} 400 - Jika buku tidak ditemukan atau sudah dihapus
 */
const getBookById = async id => {
  const data = await prismaClient.book.findFirst({
    where: { id: id },
    select: {
      id: true,
      title: true,
      author: true,
      description: true,
      isbn: true,
      stok: true,
      deteled_at: true,
      genres: {
        select: {
          genre: {
            select: {
              name: true,
            },
          },
        },
      },
      publisher: {
        select: {
          name: true,
        },
      },
    },
  });

  if (!data || data.deleted_at === null) {
    throw new ResponseError(400, "No book found with id " + req.params.id);
  }

  return {
    id: data.id,
    title: data.title,
    author: data.author,
    description: data.description,
    isbn: data.isbn,
    stok: data.stok,
    genres: data.genres.map(genre => genre.genre.name),
    publisher: data.publisher?.name || "Unknown",
  };
};

// TODO: Melihat member lain yang aktif

/**
 * Meminjamkan buku
 *
 * Fungsi untuk meminjam buku. Mengecek apakah buku masih tersedia, apakah pengguna sudah meminjam buku yang sama,
 * dan apakah stok buku mencukupi. Jika semua validasi lulus, transaksi peminjaman akan diproses dan stok buku akan dikurangi.
 *
 * @async
 * @function loanBook
 * @param {object} req - Request objek yang berisi data peminjaman buku
 * @param {object} res - Response objek yang akan mengirimkan hasil operasi ke client
 * @returns {Promise<object>} Data peminjaman yang berhasil
 * @throws {ResponseError} 400 - Jika buku sudah dipinjam atau stok buku habis
 * @throws {ResponseError} 404 - Jika buku tidak ditemukan atau sudah dihapus
 */
const loanBook = async (req, res) => {
  const data = validate(loanValidation, req.body);

  const checkBookAlreadyDeleted = await prismaClient.book.findFirst({
    where: {
      book_id: data.book_id,
    },
  });

  if (!checkBookAlreadyDeleted) {
    throw new ResponseError(
      404,
      "The book is already deleted or does not exist.",
    );
  }

  const isBookAlreadyBorrowedByStudent = await prismaClient.peminjaman.count({
    where: {
      book_id: data.book_id,
      student_id: data.student_id,
    },
  });

  if (isBookAlreadyBorrowedByStudent >= 1) {
    throw new ResponseError(
      400,
      "The book is already borrowed. Please return it first.",
    );
  }

  const stockBook = await prismaClient.book.findFirst({
    where: { id: data.book_id },
    select: { stok: true },
  });

  if (stockBook.stok <= 0) {
    throw new ResponseError(400, "Book is out of stock.");
  }

  const [loan, updateStock] = await prismaClient.$transaction([
    prismaClient.peminjaman.create({
      data: {
        borrow_date: new Date(),
        return_date: new Date(),
        student_id: data.student_id,
        book_id: data.book_id,
        notes: data.notes,
      },
    }),
    prismaClient.book.update({
      where: { id: data.book_id },
      data: {
        stok: {
          decrement: 1,
        },
      },
    }),
  ]);

  if (!updateStock) {
    throw new ResponseError(500, "Error occurred while updating book stock.");
  }
  return loan;
};

/**
 * Mengembalikan buku
 *
 * Fungsi untuk mengembalikan buku yang sudah dipinjam. Mengecek apakah buku yang dikembalikan benar-benar dipinjam oleh pengguna
 * dan memperbarui stok buku yang dikembalikan.
 *
 * @async
 * @function returnBook
 * @param {object} req - Request objek yang berisi data pengembalian buku
 * @param {object} res - Response objek yang akan mengirimkan hasil operasi ke client
 * @returns {Promise<object>} Pesan sukses pengembalian buku
 * @throws {ResponseError} 400 - Jika buku yang dikembalikan tidak pernah dipinjam
 */
const returnBook = async (req, res) => {
  const data = validate(returnValidation, req.body);

  const isBookBorrowedByStudent = await prismaClient.peminjaman.count({
    where: {
      book_id: data.book_id,
      student_id: data.student_id,
    },
  });

  if (isBookBorrowedByStudent < 1) {
    throw new ResponseError(400, "The user has not borrowed this book.");
  }

  const updateBook = await prismaClient.$transaction([
    prismaClient.book.update({
      where: { id: data.book_id },
      data: { stok: { increment: 1 } },
    }),
    prismaClient.peminjaman.delete({
      where: {
        student_id_book_id: {
          student_id: data.student_id,
          book_id: data.book_id,
        },
      },
    }),
  ]);

  return { message: "Book successfully returned and stock updated." };
};

export default {
  registrasi,
  login,
  getBookList,
  getBookById,
  loanBook,
  returnBook,
};