From 183969cfd73560cc3ac1b5382a38f7d15520a473 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Oct 2025 18:49:21 +0000 Subject: [PATCH 1/2] feat: Initialize room reservation project structure This commit sets up the basic project structure, including database schema, Maven configuration, main application class, model entities, repositories, and a booking service with initial logic. Co-authored-by: valcomman --- room-reservation/db/schema.sql | 105 ++++++++++++++++++ room-reservation/pom.xml | 99 +++++++++++++++++ .../RoomReservationApplication.java | 12 ++ .../roomreservation/model/Apartment.java | 53 +++++++++ .../roomreservation/model/AuditLog.java | 47 ++++++++ .../roomreservation/model/Booking.java | 102 +++++++++++++++++ .../roomreservation/model/BookingStatus.java | 9 ++ .../roomreservation/model/Building.java | 30 +++++ .../roomreservation/model/Payment.java | 62 +++++++++++ .../model/PaymentProvider.java | 9 ++ .../roomreservation/model/PaymentStatus.java | 7 ++ .../building/roomreservation/model/Role.java | 7 ++ .../building/roomreservation/model/Room.java | 39 +++++++ .../building/roomreservation/model/User.java | 52 +++++++++ .../repository/ApartmentRepository.java | 6 + .../repository/AuditLogRepository.java | 6 + .../repository/BookingRepository.java | 25 +++++ .../repository/BuildingRepository.java | 6 + .../repository/PaymentRepository.java | 6 + .../repository/RoomRepository.java | 6 + .../repository/UserRepository.java | 10 ++ .../service/BookingService.java | 105 ++++++++++++++++++ .../src/main/resources/application.yml | 47 ++++++++ .../service/BookingServiceTest.java | 90 +++++++++++++++ 24 files changed, 940 insertions(+) create mode 100644 room-reservation/db/schema.sql create mode 100644 room-reservation/pom.xml create mode 100644 room-reservation/src/main/java/com/building/roomreservation/RoomReservationApplication.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/Apartment.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/AuditLog.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/Booking.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/BookingStatus.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/Building.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/Payment.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/PaymentProvider.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/PaymentStatus.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/Role.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/Room.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/model/User.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/ApartmentRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/AuditLogRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/BookingRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/BuildingRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/PaymentRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/RoomRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/repository/UserRepository.java create mode 100644 room-reservation/src/main/java/com/building/roomreservation/service/BookingService.java create mode 100644 room-reservation/src/main/resources/application.yml create mode 100644 room-reservation/src/test/java/com/building/roomreservation/service/BookingServiceTest.java diff --git a/room-reservation/db/schema.sql b/room-reservation/db/schema.sql new file mode 100644 index 0000000..1ae8e55 --- /dev/null +++ b/room-reservation/db/schema.sql @@ -0,0 +1,105 @@ +-- SQL Server DDL for Room Reservation App + +CREATE TABLE buildings ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(200) NOT NULL, + address NVARCHAR(400) NULL, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() +); + +CREATE TABLE apartments ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + building_id BIGINT NOT NULL, + floor INT NOT NULL, + apartment_number NVARCHAR(50) NOT NULL, + owner_name NVARCHAR(200) NULL, + contact_email NVARCHAR(200) NULL, + contact_phone NVARCHAR(50) NULL, + is_active BIT NOT NULL DEFAULT 1, + CONSTRAINT uq_apartment UNIQUE (building_id, apartment_number), + CONSTRAINT fk_apartment_building FOREIGN KEY (building_id) REFERENCES buildings(id) +); + +CREATE TABLE users ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + email NVARCHAR(255) NOT NULL UNIQUE, + password_hash NVARCHAR(255) NOT NULL, + role NVARCHAR(32) NOT NULL, + apartment_id BIGINT NULL, + full_name NVARCHAR(200) NULL, + phone NVARCHAR(50) NULL, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + CONSTRAINT fk_user_apartment FOREIGN KEY (apartment_id) REFERENCES apartments(id) +); + +CREATE TABLE rooms ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(200) NOT NULL, + capacity INT NOT NULL, + description NVARCHAR(1000) NULL, + is_active BIT NOT NULL DEFAULT 1, + rules_json NVARCHAR(MAX) NULL +); + +CREATE TYPE booking_status AS TABLE ( + status NVARCHAR(16) +); +-- Note: Using plain NVARCHAR for enums in SQL Server; enforce via check constraint + +CREATE TABLE bookings ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + booking_number BIGINT NULL, + room_id BIGINT NOT NULL, + apartment_id BIGINT NOT NULL, + requested_start DATETIME2 NOT NULL, + requested_end DATETIME2 NOT NULL, + status NVARCHAR(16) NOT NULL, + event_type NVARCHAR(100) NULL, + num_guests INT NULL, + notes NVARCHAR(2000) NULL, + created_by BIGINT NOT NULL, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + updated_at DATETIME2 NULL, + approved_by BIGINT NULL, + approved_at DATETIME2 NULL, + payment_status NVARCHAR(16) NOT NULL DEFAULT 'NOT_PAID', + CONSTRAINT fk_booking_room FOREIGN KEY (room_id) REFERENCES rooms(id), + CONSTRAINT fk_booking_apartment FOREIGN KEY (apartment_id) REFERENCES apartments(id), + CONSTRAINT fk_booking_created_by FOREIGN KEY (created_by) REFERENCES users(id), + CONSTRAINT fk_booking_approved_by FOREIGN KEY (approved_by) REFERENCES users(id), + CONSTRAINT ck_booking_time CHECK (requested_end > requested_start), + CONSTRAINT ck_booking_status CHECK (status IN ('PENDING','APPROVED','REJECTED','CANCELLED','COMPLETED')), + CONSTRAINT ck_booking_payment CHECK (payment_status IN ('NOT_PAID','PAID','REFUNDED')) +); + +-- Indexes for overlap queries +CREATE INDEX ix_bookings_room_time ON bookings (room_id, requested_start, requested_end); +CREATE INDEX ix_bookings_status ON bookings (status); +CREATE INDEX ix_bookings_apartment ON bookings (apartment_id); + +CREATE TABLE payments ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + booking_id BIGINT NOT NULL, + amount DECIMAL(12,2) NOT NULL, + currency NVARCHAR(8) NOT NULL, + status NVARCHAR(16) NOT NULL, + provider NVARCHAR(50) NOT NULL, + transaction_id NVARCHAR(128) NULL, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + processed_at DATETIME2 NULL, + CONSTRAINT fk_payment_booking FOREIGN KEY (booking_id) REFERENCES bookings(id), + CONSTRAINT ck_payment_status CHECK (status IN ('INIT','SUCCESS','FAILED','REFUNDED')) +); + +CREATE TABLE audit_logs ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + entity_type NVARCHAR(64) NOT NULL, + entity_id BIGINT NOT NULL, + action NVARCHAR(64) NOT NULL, + actor_user_id BIGINT NULL, + details NVARCHAR(MAX) NULL, + created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() +); + +-- Optional sequences +CREATE SEQUENCE booking_number_seq AS BIGINT START WITH 1000 INCREMENT BY 1; diff --git a/room-reservation/pom.xml b/room-reservation/pom.xml new file mode 100644 index 0000000..3d5476e --- /dev/null +++ b/room-reservation/pom.xml @@ -0,0 +1,99 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + + com.building + room-reservation + 0.1.0-SNAPSHOT + room-reservation + Room Reservation App + + + 17 + 1.20.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-security + provided + + + + + com.microsoft.sqlserver + mssql-jdbc + runtime + + + + + com.h2database + h2 + test + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mssqlserver + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + diff --git a/room-reservation/src/main/java/com/building/roomreservation/RoomReservationApplication.java b/room-reservation/src/main/java/com/building/roomreservation/RoomReservationApplication.java new file mode 100644 index 0000000..503cc00 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/RoomReservationApplication.java @@ -0,0 +1,12 @@ +package com.building.roomreservation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RoomReservationApplication { + + public static void main(String[] args) { + SpringApplication.run(RoomReservationApplication.class, args); + } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/Apartment.java b/room-reservation/src/main/java/com/building/roomreservation/model/Apartment.java new file mode 100644 index 0000000..a31f3f4 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/Apartment.java @@ -0,0 +1,53 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "apartments", uniqueConstraints = { + @UniqueConstraint(name = "uq_apartment", columnNames = {"building_id", "apartment_number"}) +}) +public class Apartment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "building_id") + private Building building; + + @Column(nullable = false) + private int floor; + + @Column(name = "apartment_number", nullable = false) + private String apartmentNumber; + + @Column(name = "owner_name") + private String ownerName; + + @Column(name = "contact_email") + private String contactEmail; + + @Column(name = "contact_phone") + private String contactPhone; + + @Column(name = "is_active", nullable = false) + private boolean active = true; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Building getBuilding() { return building; } + public void setBuilding(Building building) { this.building = building; } + public int getFloor() { return floor; } + public void setFloor(int floor) { this.floor = floor; } + public String getApartmentNumber() { return apartmentNumber; } + public void setApartmentNumber(String apartmentNumber) { this.apartmentNumber = apartmentNumber; } + public String getOwnerName() { return ownerName; } + public void setOwnerName(String ownerName) { this.ownerName = ownerName; } + public String getContactEmail() { return contactEmail; } + public void setContactEmail(String contactEmail) { this.contactEmail = contactEmail; } + public String getContactPhone() { return contactPhone; } + public void setContactPhone(String contactPhone) { this.contactPhone = contactPhone; } + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/AuditLog.java b/room-reservation/src/main/java/com/building/roomreservation/model/AuditLog.java new file mode 100644 index 0000000..3637a31 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/AuditLog.java @@ -0,0 +1,47 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "audit_logs") +public class AuditLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "entity_type", nullable = false) + private String entityType; + + @Column(name = "entity_id", nullable = false) + private Long entityId; + + @Column(nullable = false) + private String action; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "actor_user_id") + private User actor; + + @Column(columnDefinition = "TEXT") + private String details; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt = OffsetDateTime.now(); + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getEntityType() { return entityType; } + public void setEntityType(String entityType) { this.entityType = entityType; } + public Long getEntityId() { return entityId; } + public void setEntityId(Long entityId) { this.entityId = entityId; } + public String getAction() { return action; } + public void setAction(String action) { this.action = action; } + public User getActor() { return actor; } + public void setActor(User actor) { this.actor = actor; } + public String getDetails() { return details; } + public void setDetails(String details) { this.details = details; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/Booking.java b/room-reservation/src/main/java/com/building/roomreservation/model/Booking.java new file mode 100644 index 0000000..072ad1c --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/Booking.java @@ -0,0 +1,102 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "bookings", + indexes = { + @Index(name = "ix_bookings_room_time", columnList = "room_id, requested_start, requested_end"), + @Index(name = "ix_bookings_status", columnList = "status"), + @Index(name = "ix_bookings_apartment", columnList = "apartment_id") + }) +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "booking_number") + private Long bookingNumber; // nullable until approved + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "room_id") + private Room room; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "apartment_id") + private Apartment apartment; + + @Column(name = "requested_start", nullable = false) + private OffsetDateTime requestedStart; + + @Column(name = "requested_end", nullable = false) + private OffsetDateTime requestedEnd; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private BookingStatus status = BookingStatus.PENDING; + + @Column(name = "event_type") + private String eventType; + + @Column(name = "num_guests") + private Integer numGuests; + + @Column(length = 2000) + private String notes; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "created_by") + private User createdBy; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt = OffsetDateTime.now(); + + @Column(name = "updated_at") + private OffsetDateTime updatedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "approved_by") + private User approvedBy; + + @Column(name = "approved_at") + private OffsetDateTime approvedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false) + private PaymentStatus paymentStatus = PaymentStatus.NOT_PAID; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Long getBookingNumber() { return bookingNumber; } + public void setBookingNumber(Long bookingNumber) { this.bookingNumber = bookingNumber; } + public Room getRoom() { return room; } + public void setRoom(Room room) { this.room = room; } + public Apartment getApartment() { return apartment; } + public void setApartment(Apartment apartment) { this.apartment = apartment; } + public OffsetDateTime getRequestedStart() { return requestedStart; } + public void setRequestedStart(OffsetDateTime requestedStart) { this.requestedStart = requestedStart; } + public OffsetDateTime getRequestedEnd() { return requestedEnd; } + public void setRequestedEnd(OffsetDateTime requestedEnd) { this.requestedEnd = requestedEnd; } + public BookingStatus getStatus() { return status; } + public void setStatus(BookingStatus status) { this.status = status; } + public String getEventType() { return eventType; } + public void setEventType(String eventType) { this.eventType = eventType; } + public Integer getNumGuests() { return numGuests; } + public void setNumGuests(Integer numGuests) { this.numGuests = numGuests; } + public String getNotes() { return notes; } + public void setNotes(String notes) { this.notes = notes; } + public User getCreatedBy() { return createdBy; } + public void setCreatedBy(User createdBy) { this.createdBy = createdBy; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + public OffsetDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; } + public User getApprovedBy() { return approvedBy; } + public void setApprovedBy(User approvedBy) { this.approvedBy = approvedBy; } + public OffsetDateTime getApprovedAt() { return approvedAt; } + public void setApprovedAt(OffsetDateTime approvedAt) { this.approvedAt = approvedAt; } + public PaymentStatus getPaymentStatus() { return paymentStatus; } + public void setPaymentStatus(PaymentStatus paymentStatus) { this.paymentStatus = paymentStatus; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/BookingStatus.java b/room-reservation/src/main/java/com/building/roomreservation/model/BookingStatus.java new file mode 100644 index 0000000..568b669 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/BookingStatus.java @@ -0,0 +1,9 @@ +package com.building.roomreservation.model; + +public enum BookingStatus { + PENDING, + APPROVED, + REJECTED, + CANCELLED, + COMPLETED +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/Building.java b/room-reservation/src/main/java/com/building/roomreservation/model/Building.java new file mode 100644 index 0000000..676f54d --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/Building.java @@ -0,0 +1,30 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "buildings") +public class Building { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + private String address; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt = OffsetDateTime.now(); + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/Payment.java b/room-reservation/src/main/java/com/building/roomreservation/model/Payment.java new file mode 100644 index 0000000..a902f1b --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/Payment.java @@ -0,0 +1,62 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "payments") +public class Payment { + + public enum Status { INIT, SUCCESS, FAILED, REFUNDED } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "booking_id") + private Booking booking; + + @Column(nullable = false, precision = 12, scale = 2) + private BigDecimal amount; + + @Column(nullable = false, length = 8) + private String currency; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Status status = Status.INIT; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PaymentProvider provider = PaymentProvider.OTHER; + + @Column(name = "transaction_id", length = 128) + private String transactionId; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt = OffsetDateTime.now(); + + @Column(name = "processed_at") + private OffsetDateTime processedAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Booking getBooking() { return booking; } + public void setBooking(Booking booking) { this.booking = booking; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public String getCurrency() { return currency; } + public void setCurrency(String currency) { this.currency = currency; } + public Status getStatus() { return status; } + public void setStatus(Status status) { this.status = status; } + public PaymentProvider getProvider() { return provider; } + public void setProvider(PaymentProvider provider) { this.provider = provider; } + public String getTransactionId() { return transactionId; } + public void setTransactionId(String transactionId) { this.transactionId = transactionId; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + public OffsetDateTime getProcessedAt() { return processedAt; } + public void setProcessedAt(OffsetDateTime processedAt) { this.processedAt = processedAt; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/PaymentProvider.java b/room-reservation/src/main/java/com/building/roomreservation/model/PaymentProvider.java new file mode 100644 index 0000000..2c6d414 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/PaymentProvider.java @@ -0,0 +1,9 @@ +package com.building.roomreservation.model; + +public enum PaymentProvider { + STRIPE, + PAYPAL, + PAYME, + ISRCARD, + OTHER +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/PaymentStatus.java b/room-reservation/src/main/java/com/building/roomreservation/model/PaymentStatus.java new file mode 100644 index 0000000..e642bf5 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/PaymentStatus.java @@ -0,0 +1,7 @@ +package com.building.roomreservation.model; + +public enum PaymentStatus { + NOT_PAID, + PAID, + REFUNDED +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/Role.java b/room-reservation/src/main/java/com/building/roomreservation/model/Role.java new file mode 100644 index 0000000..9bdfde3 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/Role.java @@ -0,0 +1,7 @@ +package com.building.roomreservation.model; + +public enum Role { + TENANT, + ADMIN, + SUPER_ADMIN +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/Room.java b/room-reservation/src/main/java/com/building/roomreservation/model/Room.java new file mode 100644 index 0000000..a2eff4b --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/Room.java @@ -0,0 +1,39 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "rooms") +public class Room { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private int capacity; + + private String description; + + @Column(name = "is_active", nullable = false) + private boolean active = true; + + @Column(name = "rules_json", columnDefinition = "TEXT") + private String rulesJson; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getCapacity() { return capacity; } + public void setCapacity(int capacity) { this.capacity = capacity; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + public String getRulesJson() { return rulesJson; } + public void setRulesJson(String rulesJson) { this.rulesJson = rulesJson; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/model/User.java b/room-reservation/src/main/java/com/building/roomreservation/model/User.java new file mode 100644 index 0000000..2a52b51 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/model/User.java @@ -0,0 +1,52 @@ +package com.building.roomreservation.model; + +import jakarta.persistence.*; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + @Column(name = "password_hash", nullable = false) + private String passwordHash; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role = Role.TENANT; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "apartment_id") + private Apartment apartment; + + @Column(name = "full_name") + private String fullName; + + private String phone; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt = OffsetDateTime.now(); + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPasswordHash() { return passwordHash; } + public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } + public Role getRole() { return role; } + public void setRole(Role role) { this.role = role; } + public Apartment getApartment() { return apartment; } + public void setApartment(Apartment apartment) { this.apartment = apartment; } + public String getFullName() { return fullName; } + public void setFullName(String fullName) { this.fullName = fullName; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/ApartmentRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/ApartmentRepository.java new file mode 100644 index 0000000..218850f --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/ApartmentRepository.java @@ -0,0 +1,6 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.Apartment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ApartmentRepository extends JpaRepository { } diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/AuditLogRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/AuditLogRepository.java new file mode 100644 index 0000000..48ae740 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/AuditLogRepository.java @@ -0,0 +1,6 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.AuditLog; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuditLogRepository extends JpaRepository { } diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/BookingRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/BookingRepository.java new file mode 100644 index 0000000..a7f93e4 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/BookingRepository.java @@ -0,0 +1,25 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.*; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; + +public interface BookingRepository extends JpaRepository { + + @Query("select b from Booking b where b.room.id = :roomId and b.status in (:statuses) and not (b.requestedEnd <= :start or b.requestedStart >= :end)") + List findOverlapping(@Param("roomId") Long roomId, + @Param("start") OffsetDateTime start, + @Param("end") OffsetDateTime end, + @Param("statuses") List statuses); + + @Query("select max(b.bookingNumber) from Booking b") + Optional findMaxBookingNumber(); + + @Query("select max(b.requestedEnd) from Booking b where b.apartment.id = :apartmentId and b.room.id = :roomId and b.status in (com.building.roomreservation.model.BookingStatus.APPROVED, com.building.roomreservation.model.BookingStatus.COMPLETED)") + OffsetDateTime findLastUsage(@Param("apartmentId") Long apartmentId, @Param("roomId") Long roomId); +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/BuildingRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/BuildingRepository.java new file mode 100644 index 0000000..0206f22 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/BuildingRepository.java @@ -0,0 +1,6 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.Building; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BuildingRepository extends JpaRepository { } diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/PaymentRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/PaymentRepository.java new file mode 100644 index 0000000..7c87582 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/PaymentRepository.java @@ -0,0 +1,6 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.Payment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentRepository extends JpaRepository { } diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/RoomRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/RoomRepository.java new file mode 100644 index 0000000..fb0ab12 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/RoomRepository.java @@ -0,0 +1,6 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.Room; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RoomRepository extends JpaRepository { } diff --git a/room-reservation/src/main/java/com/building/roomreservation/repository/UserRepository.java b/room-reservation/src/main/java/com/building/roomreservation/repository/UserRepository.java new file mode 100644 index 0000000..462cef9 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.building.roomreservation.repository; + +import com.building.roomreservation.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/room-reservation/src/main/java/com/building/roomreservation/service/BookingService.java b/room-reservation/src/main/java/com/building/roomreservation/service/BookingService.java new file mode 100644 index 0000000..ba6ac21 --- /dev/null +++ b/room-reservation/src/main/java/com/building/roomreservation/service/BookingService.java @@ -0,0 +1,105 @@ +package com.building.roomreservation.service; + +import com.building.roomreservation.model.*; +import com.building.roomreservation.repository.BookingRepository; +import com.building.roomreservation.repository.RoomRepository; +import jakarta.persistence.LockModeType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.OffsetDateTime; +import java.util.*; + +@Service +public class BookingService { + + private final BookingRepository bookingRepository; + private final RoomRepository roomRepository; + + public BookingService(BookingRepository bookingRepository, RoomRepository roomRepository) { + this.bookingRepository = bookingRepository; + this.roomRepository = roomRepository; + } + + @Transactional(readOnly = true) + public List findOverlaps(Long roomId, OffsetDateTime start, OffsetDateTime end) { + return bookingRepository.findOverlapping(roomId, start, end, + Arrays.asList(BookingStatus.PENDING, BookingStatus.APPROVED, BookingStatus.COMPLETED)); + } + + @Transactional + public Booking createRequest(Booking booking) { + if (!booking.getRequestedEnd().isAfter(booking.getRequestedStart())) { + throw new IllegalArgumentException("End must be after start"); + } + List conflicts = findOverlaps(booking.getRoom().getId(), booking.getRequestedStart(), booking.getRequestedEnd()); + boolean hasApprovedConflict = conflicts.stream().anyMatch(b -> b.getStatus() == BookingStatus.APPROVED || b.getStatus() == BookingStatus.COMPLETED); + if (hasApprovedConflict) { + booking.setStatus(BookingStatus.REJECTED); + return bookingRepository.save(booking); + } + booking.setStatus(BookingStatus.PENDING); + return bookingRepository.save(booking); + } + + @Transactional + public Booking approve(Long bookingId, User adminUser) { + Booking booking = bookingRepository.findById(bookingId).orElseThrow(); + if (booking.getStatus() != BookingStatus.PENDING) { + throw new IllegalStateException("Only PENDING bookings can be approved"); + } + // Re-check conflicts inside transaction + List conflicts = findOverlaps(booking.getRoom().getId(), booking.getRequestedStart(), booking.getRequestedEnd()); + boolean approvedExists = conflicts.stream() + .anyMatch(b -> !Objects.equals(b.getId(), booking.getId()) && (b.getStatus() == BookingStatus.APPROVED || b.getStatus() == BookingStatus.COMPLETED)); + if (approvedExists) { + throw new IllegalStateException("Slot already approved for another booking"); + } + booking.setStatus(BookingStatus.APPROVED); + booking.setApprovedBy(adminUser); + booking.setApprovedAt(OffsetDateTime.now()); + booking.setBookingNumber(nextBookingNumber()); + return bookingRepository.save(booking); + } + + private Long nextBookingNumber() { + Optional max = bookingRepository.findMaxBookingNumber(); + return max.map(aLong -> aLong + 1).orElse(1000L); + } + + @Transactional + public Optional allocatePriorityForSlot(Long roomId, OffsetDateTime start, OffsetDateTime end) { + List candidates = bookingRepository.findOverlapping(roomId, start, end, Collections.singletonList(BookingStatus.PENDING)); + if (candidates.isEmpty()) return Optional.empty(); + + candidates.sort((a, b) -> { + OffsetDateTime lastA = bookingRepository.findLastUsage(a.getApartment().getId(), roomId); + OffsetDateTime lastB = bookingRepository.findLastUsage(b.getApartment().getId(), roomId); + int cmp; + if (lastA == null && lastB == null) cmp = 0; + else if (lastA == null) cmp = -1; // never used gets priority + else if (lastB == null) cmp = 1; + else cmp = lastA.compareTo(lastB); // older last usage first + if (cmp != 0) return cmp; + // fallback: createdAt + cmp = a.getCreatedAt().compareTo(b.getCreatedAt()); + if (cmp != 0) return cmp; + // final fallback: id + return a.getId().compareTo(b.getId()); + }); + + Booking winner = candidates.get(0); + winner.setStatus(BookingStatus.APPROVED); + winner.setApprovedAt(OffsetDateTime.now()); + winner.setBookingNumber(nextBookingNumber()); + bookingRepository.save(winner); + + // reject others + for (int i = 1; i < candidates.size(); i++) { + Booking other = candidates.get(i); + other.setStatus(BookingStatus.REJECTED); + bookingRepository.save(other); + } + return Optional.of(winner); + } +} diff --git a/room-reservation/src/main/resources/application.yml b/room-reservation/src/main/resources/application.yml new file mode 100644 index 0000000..d6056be --- /dev/null +++ b/room-reservation/src/main/resources/application.yml @@ -0,0 +1,47 @@ +spring: + application: + name: room-reservation + datasource: + url: jdbc:h2:mem:roomres;DB_CLOSE_DELAY=-1;MODE=MSSQLServer + driver-class-name: org.h2.Driver + username: sa + password: '' + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + jdbc: + time_zone: UTC + open-in-view: false + sql: + init: + mode: never + +server: + port: 8080 + +logging: + level: + org.hibernate.SQL: warn + org.hibernate.orm.jdbc.bind: off + +--- +# SQL Server profile (set SPRING_PROFILES_ACTIVE=sqlserver) +spring: + config: + activate: + on-profile: sqlserver + datasource: + url: jdbc:sqlserver://localhost:1433;databaseName=roomres;encrypt=true;trustServerCertificate=true + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + username: roomres_user + password: changeMe + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.SQLServerDialect + jdbc: + time_zone: UTC diff --git a/room-reservation/src/test/java/com/building/roomreservation/service/BookingServiceTest.java b/room-reservation/src/test/java/com/building/roomreservation/service/BookingServiceTest.java new file mode 100644 index 0000000..8ab9428 --- /dev/null +++ b/room-reservation/src/test/java/com/building/roomreservation/service/BookingServiceTest.java @@ -0,0 +1,90 @@ +package com.building.roomreservation.service; + +import com.building.roomreservation.model.*; +import com.building.roomreservation.repository.BookingRepository; +import com.building.roomreservation.repository.RoomRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.time.OffsetDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class BookingServiceTest { + + private BookingRepository bookingRepository; + private RoomRepository roomRepository; + private BookingService service; + + @BeforeEach + void setup() { + bookingRepository = mock(BookingRepository.class); + roomRepository = mock(RoomRepository.class); + service = new BookingService(bookingRepository, roomRepository); + } + + @Test + void createRequest_rejects_whenApprovedConflictExists() { + Room room = new Room(); room.setId(1L); + Apartment apt = new Apartment(); apt.setId(1L); + User user = new User(); user.setId(10L); + Booking incoming = new Booking(); + incoming.setRoom(room); + incoming.setApartment(apt); + incoming.setRequestedStart(OffsetDateTime.parse("2025-01-01T10:00:00Z")); + incoming.setRequestedEnd(OffsetDateTime.parse("2025-01-01T12:00:00Z")); + incoming.setCreatedBy(user); + + Booking existing = new Booking(); + existing.setId(2L); + existing.setRoom(room); + existing.setApartment(apt); + existing.setRequestedStart(OffsetDateTime.parse("2025-01-01T09:00:00Z")); + existing.setRequestedEnd(OffsetDateTime.parse("2025-01-01T11:00:00Z")); + existing.setStatus(BookingStatus.APPROVED); + + when(bookingRepository.findOverlapping(eq(1L), any(), any(), any())).thenReturn(List.of(existing)); + when(bookingRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + Booking result = service.createRequest(incoming); + assertEquals(BookingStatus.REJECTED, result.getStatus()); + } + + @Test + void allocatePriority_prefersApartmentWithOldestUsageOrNeverUsed() { + Long roomId = 1L; + Room room = new Room(); room.setId(roomId); + Apartment a1 = new Apartment(); a1.setId(1L); + Apartment a2 = new Apartment(); a2.setId(2L); + + Booking b1 = new Booking(); b1.setId(11L); b1.setApartment(a1); b1.setRoom(room); + b1.setRequestedStart(OffsetDateTime.parse("2025-01-01T10:00:00Z")); + b1.setRequestedEnd(OffsetDateTime.parse("2025-01-01T11:00:00Z")); + b1.setCreatedAt(OffsetDateTime.parse("2024-12-01T10:00:00Z")); + b1.setStatus(BookingStatus.PENDING); + + Booking b2 = new Booking(); b2.setId(12L); b2.setApartment(a2); b2.setRoom(room); + b2.setRequestedStart(OffsetDateTime.parse("2025-01-01T10:00:00Z")); + b2.setRequestedEnd(OffsetDateTime.parse("2025-01-01T11:00:00Z")); + b2.setCreatedAt(OffsetDateTime.parse("2024-12-01T09:00:00Z")); // earlier request, should lose if a1 never used + b2.setStatus(BookingStatus.PENDING); + + when(bookingRepository.findOverlapping(eq(roomId), any(), any(), any())).thenReturn(List.of(b1, b2)); + when(bookingRepository.findLastUsage(eq(1L), eq(roomId))).thenReturn(null); // never used -> highest priority + when(bookingRepository.findLastUsage(eq(2L), eq(roomId))).thenReturn(OffsetDateTime.parse("2024-01-01T00:00:00Z")); + when(bookingRepository.findMaxBookingNumber()).thenReturn(Optional.of(1500L)); + when(bookingRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + Optional winnerOpt = service.allocatePriorityForSlot(roomId, + OffsetDateTime.parse("2025-01-01T10:00:00Z"), OffsetDateTime.parse("2025-01-01T11:00:00Z")); + + assertTrue(winnerOpt.isPresent()); + Booking winner = winnerOpt.get(); + assertEquals(a1.getId(), winner.getApartment().getId()); + assertEquals(BookingStatus.APPROVED, winner.getStatus()); + assertEquals(1501L, winner.getBookingNumber()); + } +} From 425a017541160b2eaaf2e3d6aef6a800453f816a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Oct 2025 18:56:47 +0000 Subject: [PATCH 2/2] Add Docker support, CI, and prod profile Co-authored-by: valcomman --- .github/workflows/room-reservation-ci.yml | 49 +++++++++++++++++++ room-reservation/.dockerignore | 18 +++++++ room-reservation/Dockerfile | 28 +++++++++++ room-reservation/README.md | 38 ++++++++++++++ room-reservation/docker-compose.yml | 36 ++++++++++++++ room-reservation/pom.xml | 7 +++ .../src/main/resources/application-prod.yml | 24 +++++++++ 7 files changed, 200 insertions(+) create mode 100644 .github/workflows/room-reservation-ci.yml create mode 100644 room-reservation/.dockerignore create mode 100644 room-reservation/Dockerfile create mode 100644 room-reservation/README.md create mode 100644 room-reservation/docker-compose.yml create mode 100644 room-reservation/src/main/resources/application-prod.yml diff --git a/.github/workflows/room-reservation-ci.yml b/.github/workflows/room-reservation-ci.yml new file mode 100644 index 0000000..28cb193 --- /dev/null +++ b/.github/workflows/room-reservation-ci.yml @@ -0,0 +1,49 @@ +name: Room Reservation CI + +on: + push: + branches: [ "**" ] + pull_request: + +permissions: + contents: read + packages: write + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: room-reservation + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + - name: Build and test + run: mvn -B -ntp clean verify + + docker: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Compute image repo (lowercase) + run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}/room-reservation" >> $GITHUB_ENV + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and (conditionally) push image + uses: docker/build-push-action@v6 + with: + context: ./room-reservation + push: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }} + tags: ${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.sha }} diff --git a/room-reservation/.dockerignore b/room-reservation/.dockerignore new file mode 100644 index 0000000..d80754f --- /dev/null +++ b/room-reservation/.dockerignore @@ -0,0 +1,18 @@ +# Maven +.target +**/target +.mvn +**/.mvn + +# Git +.git +.gitignore + +# IDE +.idea +*.iml +.vscode + +# OS +.DS_Store +Thumbs.db diff --git a/room-reservation/Dockerfile b/room-reservation/Dockerfile new file mode 100644 index 0000000..48e79ef --- /dev/null +++ b/room-reservation/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1.7 + +# ---------------------- +# Build stage +# ---------------------- +FROM maven:3.9.8-eclipse-temurin-17 AS build +WORKDIR /app + +COPY pom.xml . +COPY src ./src + +# Use Maven cache to speed up builds +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp clean package + +# ---------------------- +# Runtime stage +# ---------------------- +FROM eclipse-temurin:17-jre-alpine +ENV JAVA_OPTS="" +ENV SPRING_PROFILES_ACTIVE=prod +WORKDIR /app + +# Copy fat jar built by Spring Boot plugin +COPY --from=build /app/target/room-reservation-0.1.0-SNAPSHOT.jar /app/app.jar + +EXPOSE 8080 +ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar"] diff --git a/room-reservation/README.md b/room-reservation/README.md new file mode 100644 index 0000000..c0ad55f --- /dev/null +++ b/room-reservation/README.md @@ -0,0 +1,38 @@ +# Room Reservation App + +## Run locally (Docker Compose) + +Prereqs: Docker Desktop/Engine + +```bash +# from repo root or room-reservation directory +cd room-reservation + +# Build and start Postgres + app +docker compose up --build + +# App is now on http://localhost:8080 +``` + +To stop and remove containers: +```bash +docker compose down -v +``` + +## Config +- Active profile: `prod` (Postgres) +- DB creds: user `roomres`, password `roomres`, db `roomres` +- Change via env vars: `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD` + +## Build jar locally +```bash +mvn -B -ntp clean package +``` + +## CI +GitHub Actions workflow `room-reservation-ci.yml` builds and tests on every branch. On `main`/`master`, it also builds and pushes a Docker image to GHCR at `ghcr.io///room-reservation:latest`. + +## Next steps +- Add REST controllers and DTOs +- Wire authentication and payments +- Add frontend diff --git a/room-reservation/docker-compose.yml b/room-reservation/docker-compose.yml new file mode 100644 index 0000000..2d2e5c5 --- /dev/null +++ b/room-reservation/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" +services: + db: + image: postgres:16-alpine + container_name: roomres_db + environment: + POSTGRES_DB: roomres + POSTGRES_USER: roomres + POSTGRES_PASSWORD: roomres + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U roomres -d roomres"] + interval: 10s + timeout: 5s + retries: 5 + + app: + build: . + container_name: roomres_app + depends_on: + db: + condition: service_healthy + environment: + SPRING_PROFILES_ACTIVE: prod + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/roomres + SPRING_DATASOURCE_USERNAME: roomres + SPRING_DATASOURCE_PASSWORD: roomres + JAVA_OPTS: -Xms256m -Xmx512m + ports: + - "8080:8080" + +volumes: + pgdata: {} diff --git a/room-reservation/pom.xml b/room-reservation/pom.xml index 3d5476e..59ca01e 100644 --- a/room-reservation/pom.xml +++ b/room-reservation/pom.xml @@ -50,6 +50,13 @@ runtime + + + org.postgresql + postgresql + runtime + + com.h2database diff --git a/room-reservation/src/main/resources/application-prod.yml b/room-reservation/src/main/resources/application-prod.yml new file mode 100644 index 0000000..5d382d0 --- /dev/null +++ b/room-reservation/src/main/resources/application-prod.yml @@ -0,0 +1,24 @@ +spring: + datasource: + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://db:5432/roomres} + driver-class-name: org.postgresql.Driver + username: ${SPRING_DATASOURCE_USERNAME:roomres} + password: ${SPRING_DATASOURCE_PASSWORD:roomres} + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + jdbc: + time_zone: UTC + sql: + init: + mode: never + +server: + port: 8080 + +logging: + level: + org.hibernate.SQL: warn