Skip to content

Commit

Permalink
feat: support loan books
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Dec 8, 2023
1 parent 2a61da2 commit 96c990c
Show file tree
Hide file tree
Showing 114 changed files with 1,795 additions and 673 deletions.
1 change: 1 addition & 0 deletions prisma/migrations/20231111180728_initial/migration.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ CREATE TABLE "User" (
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"version" INTEGER NOT NULL DEFAULT 0,

CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
Expand Down
5 changes: 5 additions & 0 deletions prisma/migrations/20231122133945_book/migration.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
-- CreateEnum
CREATE TYPE "BookState" AS ENUM ('AVAILABLE', 'LOANED');

-- CreateTable
CREATE TABLE "Book" (
"id" TEXT NOT NULL,
"authors" TEXT[],
"image" TEXT NOT NULL,
"title" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 0,
"state" "BookState" NOT NULL DEFAULT 'AVAILABLE',

CONSTRAINT "Book_pkey" PRIMARY KEY ("id")
);
31 changes: 0 additions & 31 deletions prisma/migrations/20231127175740_loan/migration.sql

This file was deleted.

37 changes: 37 additions & 0 deletions prisma/migrations/20231203103508_loans/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- CreateTable
CREATE TABLE "Loan" (
"id" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 0,
"startsAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"bookId" TEXT NOT NULL,

CONSTRAINT "Loan_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "LoanRegistry" (
"id" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 0,
"startsAt" TIMESTAMPTZ NOT NULL,
"finishedAt" TIMESTAMPTZ NOT NULL,
"userId" TEXT NOT NULL,
"bookId" TEXT NOT NULL,

CONSTRAINT "LoanRegistry_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Loan_bookId_key" ON "Loan"("bookId");

-- AddForeignKey
ALTER TABLE "Loan" ADD CONSTRAINT "Loan_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Loan" ADD CONSTRAINT "Loan_bookId_fkey" FOREIGN KEY ("bookId") REFERENCES "Book"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "LoanRegistry" ADD CONSTRAINT "LoanRegistry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "LoanRegistry" ADD CONSTRAINT "LoanRegistry_bookId_fkey" FOREIGN KEY ("bookId") REFERENCES "Book"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
54 changes: 37 additions & 17 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ model Session {
}

model User {
id String @id @default(cuid())
id String @id @default(cuid())
version Int @default(0)
name String?
email String? @unique
email String? @unique
emailVerified DateTime?
image String?
roles String[] @default(["ROLE_USER"])
roles String[] @default(["ROLE_USER"])
accounts Account[]
sessions Session[]
Loan Loan[]
LoanRegistry LoanRegistry[]
}

model VerificationToken {
Expand All @@ -55,23 +57,41 @@ model VerificationToken {
@@unique([identifier, token])
}

model Book {
id String @id @default(cuid())
version Int @default(0)
authors String[]
image String
title String
state BookState @default(AVAILABLE)
loan Loan?
loanRegistry LoanRegistry[]
}

model Loan {
id String @id @default(cuid())
startsAt DateTime @default(now())
finishedAt DateTime?
user User @relation(fields: [userId], references: [id])
id String @id @default(cuid())
version Int @default(0)
startsAt DateTime @default(now()) @db.Timestamptz()
user User @relation(fields: [userId], references: [id])
userId String
book Book @relation(fields: [bookId], references: [id])
bookId String
@@unique([bookId])
}

model LoanRegistry {
id String @id @default(cuid())
version Int @default(0)
startsAt DateTime @db.Timestamptz()
finishedAt DateTime @db.Timestamptz()
user User @relation(fields: [userId], references: [id])
userId String
book Book @relation("Loans", fields: [bookId], references: [id])
book Book @relation(fields: [bookId], references: [id])
bookId String
activeLoan Book? @relation("ActiveLoan") @ignore
}

model Book {
id String @id @default(cuid())
authors String[]
image String
title String
Loan Loan[] @relation("Loans")
activeLoan Loan? @relation("ActiveLoan", fields: [activeLoanId], references: [id])
activeLoanId String? @unique
enum BookState {
AVAILABLE
LOANED
}
31 changes: 0 additions & 31 deletions src/components/book/index.tsx

This file was deleted.

53 changes: 53 additions & 0 deletions src/core/book/application/__tests__/create-book.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'

import CreateBookUseCase from '@/core/book/application/create-book.use-case'
import BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import DuplicateIdError from '@/core/common/domain/errors/application/duplicate-id-error'
import unexpected from '@/lib/utils/unexpected'
import BooksExamples from '@/tests/examples/books.examples'
import bookRequestExamples from '@/tests/examples/books-request.examples'

describe('CreateBookUseCase', () => {
it('should create a new book', async () => {
// Arrange
const books = new BooksInMemory()

const command = bookRequestExamples.create()
const useCase = new CreateBookUseCase(books)

// Act
const result = await useCase.with(command)

// Assert
result.match(
() => {
const savedBook = books.books.get(command.id)
expect(savedBook?.version).toEqual(0)
},
(error) => unexpected.error(error),
)
})

it('should rejects to create a book with the same id', async () => {
// Arrange
const book = BooksExamples.available()
const books = new BooksInMemory([book])

const command = {
...bookRequestExamples.create(),
id: book.id.value,
}
const useCase = new CreateBookUseCase(books)

// Act
const result = await useCase.with(command)

// Assert
result.match(
(success) => unexpected.success(success),
(error) => {
expect(error).toBeInstanceOf(DuplicateIdError)
},
)
})
})
73 changes: 73 additions & 0 deletions src/core/book/application/__tests__/loan-book.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest'

import { LoanBookUseCase } from '@/core/book/application/loan-book-use.case'
import LoanBookRequest from '@/core/book/dto/requests/loan-book.request'
import BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import ApplicationError from '@/core/common/domain/errors/application-error'
import LoanBookService from '@/core/loan/domain/services/loan-book.service'
import LoansInMemory from '@/core/loan/infrastructure/services/loans-in-memory.repository'
import unexpected from '@/lib/utils/unexpected'
import BooksExamples from '@/tests/examples/books.examples'
import LoansExamples from '@/tests/examples/loans.examples'
import UsersExamples from '@/tests/examples/users.examples'

describe('Loan book', () => {
it('should loan a available book to a user', async () => {
// Arrange
const book = BooksExamples.available()
const books = new BooksInMemory([book])

const user = UsersExamples.basic()

const loans = new LoansInMemory([])

const loanBookService = new LoanBookService(loans)

const useCase = new LoanBookUseCase(books, loanBookService)
const request = LoanBookRequest.with({
bookId: book.id.value,
userId: user.id.value,
})

// Act
const result = useCase.with(request)

// Assert
await result.match(
() => {
expect(loans.loans).toHaveLength(1)
},
(error) => unexpected.error(error),
)
})

it('should not loan an unavailable book to a user', async () => {
// Arrange
const book = BooksExamples.loaned()
const books = new BooksInMemory([book])

const user = UsersExamples.basic()

const loan = LoansExamples.ofBookAndUser(book, user)
const loans = new LoansInMemory([loan])

const loanBookService = new LoanBookService(loans)

const useCase = new LoanBookUseCase(books, loanBookService)
const request = LoanBookRequest.with({
bookId: book.id.value,
userId: user.id.value,
})

// Act
const result = useCase.with(request)

// Assert
await result.match(
(_ok) => unexpected.success(_ok),
(_error) => {
expect(_error).instanceof(ApplicationError)
},
)
})
})
42 changes: 42 additions & 0 deletions src/core/book/application/__tests__/return-book.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'

import ReturnBookUseCase from '@/core/book/application/return-book.use-case'
import ReturnBookRequest from '@/core/book/dto/requests/return-book.request'
import BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import ReturnBookService from '@/core/loan/domain/services/return-book.service'
import LoansInMemory from '@/core/loan/infrastructure/services/loans-in-memory.repository'
import unexpected from '@/lib/utils/unexpected'
import BooksExamples from '@/tests/examples/books.examples'
import LoansExamples from '@/tests/examples/loans.examples'
import UsersExamples from '@/tests/examples/users.examples'

describe('Return book', () => {
it('should return a loaned book', async () => {
// Arrange
const book = BooksExamples.loaned()
const books = new BooksInMemory([book])

const user = UsersExamples.basic()

const loan = LoansExamples.ofBookAndUser(book, user)
const loans = new LoansInMemory([loan])

const returnBookService = new ReturnBookService(loans)

const useCase = new ReturnBookUseCase(books, returnBookService)
const request = ReturnBookRequest.with({
bookId: book.id.value,
})

// Act
const result = useCase.with(request)

// Assert
await result.match(
() => {
expect(loans.loans).toHaveLength(0)
},
(error) => unexpected.error(error),
)
})
})
Loading

0 comments on commit 96c990c

Please sign in to comment.