Changes between Version 1 and Version 2 of ApplicationDevelopment


Ignore:
Timestamp:
09/04/25 22:38:20 (3 days ago)
Author:
221164
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • ApplicationDevelopment

    v1 v2  
    1 == Advanced Application Development
     1= Advanced Application Development =
     2In the latest version of the application, the following scenarios have been implemented:
     3||= ID =||= Use Case =||
     4|| 1 || View and Manage Employees ||
     5|| 2 || View and Manage Shifts ||
     6|| 3 || Assign Employees to Shifts ||
     7|| 4 || View and Manage Menu Categories ||
     8|| 5 || View and Manage Menu Products ||
     9|| 6 || View and Manage Restaurant Tables ||
     10|| 7 || Create and Manage Customer Reservations ||
     11|| 8 || Create and Manage In-House Orders (Tabs) ||
     12|| 9 || Add/Update/Remove Items from an Order ||
     13|| 10 || Process a Payment for an Order ||
     14|| 11 || View Open Orders ||
     15|| 12 || View Personal Shift Information (Employee) ||
     16|| 13 || View Sales Analytics and Reports ||
     17|| 14 || View Server Performance Statistics ||
     18|| 15 || View Monthly Revenue vs. Labor Costs ||
     19|| 16 || View Top-Selling Products ||
     20|| 17 || Manage Customer Profile and View Order History (Online) ||
     21|| 18 || Create an Online Order ||
     22
     23== Assign Employees to Shifts
     24A manager accesses the /admin/assignments page, which displays all scheduled shifts and a form to assign employees to them. The controller responsible for this is as follows:
     25
     26{{{
     27    @GetMapping("/admin/assignments")
     28    public String showAssignments(Model model) {
     29        model.addAttribute("shifts", shiftService.getAllShifts());
     30        model.addAttribute("employees", employeeService.getAllEmployees());
     31        model.addAttribute("assignments", assignmentService.getAllAssignments());
     32        model.addAttribute("assignmentForm", new CreateAssignmentDto());
     33        // In a real app, this would likely be part of a larger admin dashboard layout
     34        return "admin/assignments";
     35    }
     36}}}
     37
     38[[Image(shifts.png)]]
     39[[Image(assignments.png)]]
     40
     41After selecting a shift, an employee, and submitting the form, a POST request is sent to the controller for processing.
     42
     43{{{
     44
     45    @PostMapping("/admin/assignments")
     46    public String createAssignment(@Valid @ModelAttribute("assignmentForm") CreateAssignmentDto dto) {
     47        // The authenticated user's email would be retrieved from the SecurityContext
     48        String managerEmail = SecurityContextHolder.getContext().getAuthentication().getName();
     49        assignmentService.createAssignment(dto, managerEmail);
     50        return "redirect:/admin/assignments";
     51    }
     52}}}
     53The controller calls a function from the assignmentService to create the assignment. Inside the service, a new instance of the Assignment entity is created and populated with data from the form. To ensure data integrity (an assignment cannot exist without a valid shift, employee, and manager), the method is annotated with @Transactional. The operation will only succeed if the new Assignment object is saved to the database correctly.
     54
     55{{{
     56
     57    @Override
     58    @Transactional
     59    public Assignment createAssignment(CreateAssignmentDto dto, String managerEmail) {
     60        Manager manager = managerRepository.findByUserEmail(managerEmail)
     61                .orElseThrow(() -> new EntityNotFoundException("Manager not found"));
     62        Employee employee = employeeRepository.findById(dto.getEmployeeId())
     63                .orElseThrow(() -> new EntityNotFoundException("Employee not found"));
     64        Shift shift = shiftRepository.findById(dto.getShiftId())
     65                .orElseThrow(() -> new EntityNotFoundException("Shift not found"));
     66
     67        Assignment assignment = new Assignment();
     68        assignment.setManager(manager);
     69        assignment.setEmployee(employee);
     70        assignment.setShift(shift);
     71        // Clock-in/out times are initially null
     72
     73        return assignmentRepository.save(assignment);
     74    }
     75}}}
     76
     77The same implementation, translated into the SQL query that executes in the background, would look like this:
     78
     79{{{
     80
     81DO $$
     82    DECLARE
     83        v_manager_id BIGINT;
     84        v_employee_id BIGINT;
     85        v_shift_id BIGINT;
     86    BEGIN
     87        -- Assume manager_id=3, employee_id=1, shift_id=1 from the form
     88        v_manager_id := 3;
     89        v_employee_id := 1;
     90        v_shift_id := 1;
     91
     92        INSERT INTO assignments (manager_id, employee_id, shift_id, clock_in_time, clock_out_time)
     93        VALUES (v_manager_id, v_employee_id, v_shift_id, NULL, NULL);
     94
     95        COMMIT;
     96    END $$;
     97}}}
     98== Create and Manage In-House Orders (Tabs)
     99A server (Front Staff) who has clocked into their shift can create and manage orders for tables. When they select a table, they are presented with an interface to create a new order or manage an existing one. The controller first fetches all open orders.
     100
     101{{{
     102
     103    @GetMapping("/orders/open")
     104    public String getOpenOrders(Model model) {
     105        model.addAttribute("openOrders", orderService.findOpenOrders());
     106        return "orders/open_orders";
     107    }
     108}}}
     109[[Image(open-orders.png)]]
     110
     111To prevent unauthorized actions, the system validates all operations on the server side. When adding an item to an order, the system first verifies that the order exists and that the employee is active.
     112
     113{{{
     114
     115    @PostMapping("/orders/{orderId}/items")
     116    public String addItemToOrder(@PathVariable Long orderId, @Valid CreateOrderItemDto itemDto, Authentication authentication) {
     117        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
     118        // Custom logic to get employeeId from userDetails
     119        Long employeeId = userService.findEmployeeIdByUsername(userDetails.getUsername());
     120
     121        if (!employeeService.isOnActiveShift(employeeId)) {
     122            throw new IllegalStateException("Employee is not on an active shift.");
     123        }
     124
     125        orderService.addItemToOrder(orderId, itemDto);
     126        return "redirect:/orders/" + orderId;
     127    }
     128}}}
     129[[Image(order-details.png)]]
     130[[Image(add-items-product.png)]]
     131
     132The orderService handles the business logic of finding the product, calculating the price, and adding the new OrderItem to the Order. This entire process is wrapped in a transaction to ensure that an order item is not created unless all conditions are met (e.g., product exists, order is open).
     133
     134{{{
     135
     136    @Override
     137    @Transactional
     138    public OrderItem addItemToOrder(Long orderId, CreateOrderItemDto itemDto) {
     139        Order order = orderRepository.findById(orderId)
     140                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
     141        if (!"PENDING".equals(order.getStatus())) {
     142            throw new IllegalStateException("Order is not open for modifications.");
     143        }
     144        Product product = productRepository.findById(itemDto.getProductId())
     145                .orElseThrow(() -> new EntityNotFoundException("Product not found"));
     146
     147        OrderItem newItem = new OrderItem();
     148        newItem.setOrder(order);
     149        newItem.setProduct(product);
     150        newItem.setQuantity(itemDto.getQuantity());
     151        newItem.setPrice(product.getPrice()); // Price at the time of order
     152        newItem.setProcessed(false);
     153
     154        return orderItemRepository.save(newItem);
     155    }
     156}}}
     157The corresponding SQL operations for creating a new tab order and adding two items would be:
     158
     159{{{
     160
     161DO $$
     162    DECLARE
     163        new_order_id BIGINT;
     164        v_product_1_price DECIMAL(10,2);
     165        v_product_2_price DECIMAL(10,2);
     166    BEGIN
     167        -- Create the main order record
     168        INSERT INTO orders (status, datetime) VALUES ('PENDING', NOW())
     169        RETURNING id INTO new_order_id;
     170
     171        -- Link it as a Tab Order for a specific staff and table
     172        INSERT INTO tab_orders (order_id, front_staff_id, table_number)
     173        VALUES (new_order_id, 1, 2);
     174
     175        -- Get current prices
     176        SELECT price INTO v_product_1_price FROM products WHERE id = 1;
     177        SELECT price INTO v_product_2_price FROM products WHERE id = 2;
     178
     179        -- Add items to the order
     180        INSERT INTO order_items (order_id, product_id, is_processed, quantity, price)
     181        VALUES (new_order_id, 1, FALSE, 2, v_product_1_price);
     182
     183        INSERT INTO order_items (order_id, product_id, is_processed, quantity, price)
     184        VALUES (new_order_id, 2, FALSE, 1, v_product_2_price);
     185
     186        COMMIT;
     187    END $$;
     188}}}
     189== Process a Payment for an Order
     190When customers are ready to pay, the server initiates the payment process from the order details screen.
     191
     192[[Image(payment.png)]]
     193
     194This action is managed via a database trigger for maximum reliability. When a new record is inserted into the payments table, the payments_mark_order_paid trigger automatically fires. This ensures that an order's status is updated to PAID in the same transaction as the payment, guaranteeing that an order cannot be paid for without its status being updated accordingly.
     195
     196{{{
     197
     198    @Override
     199    @Transactional // The trigger makes this transaction encompass the order update as well
     200    public Payment createPayment(CreatePaymentDto dto) {
     201        Order order = orderRepository.findById(dto.getOrderId())
     202            .orElseThrow(() -> new EntityNotFoundException("Order not found"));
     203
     204        // Additional logic can go here, e.g., validating the payment amount
     205        // against the order total.
     206
     207        Payment payment = new Payment();
     208        payment.setOrder(order);
     209        payment.setAmount(dto.getAmount());
     210        payment.setPaymentType(dto.getPaymentType());
     211        payment.setTipAmount(dto.getTipAmount());
     212
     213        return paymentRepository.save(payment);
     214    }
     215}}}
     216The trigger and the INSERT statement are defined in SQL as follows:
     217
     218{{{
     219
     220CREATE OR REPLACE FUNCTION payments_mark_order_paid() RETURNS trigger AS $$
     221BEGIN
     222    UPDATE orders
     223    SET status = 'PAID'
     224    WHERE id = NEW.order_id;
     225    RETURN NEW;
     226END;
     227$$ LANGUAGE plpgsql;
     228
     229
     230CREATE TRIGGER trg_payments_mark_order_paid
     231    AFTER INSERT ON payments
     232    FOR EACH ROW EXECUTE FUNCTION payments_mark_order_paid();
     233}}}
     234{{{
     235-- The SQL executed by the application:
     236
     237INSERT INTO payments(order_id, amount, payment_type, tip_amount)
     238VALUES (1, 850.00, 'cash', 50.00);
     239}}}
     240
     241== View Sales Analytics and Reports
     242The public page /analytics provides data on sales performance, with options to filter by different criteria. Given that the orders, order_items, and payments tables will grow to contain a massive number of records, running complex aggregations on every page load would heavily strain the database.
     243
     244To solve this, we implemented a Materialized View (mv_payments_daily_channel) that is refreshed periodically. In our case, this could be every 30 minutes. This way, instead of the application overloading the Database Engine with expensive queries, it serves pre-calculated data directly from the view. The view is defined as follows:
     245
     246{{{
     247
     248CREATE MATERIALIZED VIEW IF NOT EXISTS mv_payments_daily_channel AS
     249WITH orders_channel AS (
     250  SELECT
     251    o.id AS order_id,
     252    CASE
     253      WHEN EXISTS (SELECT 1 FROM tab_orders t WHERE t.order_id = o.id) THEN 'TAB'
     254      WHEN EXISTS (SELECT 1 FROM online_orders oo WHERE oo.order_id = o.id) THEN 'ONLINE'
     255      ELSE 'UNKNOWN'
     256    END AS channel
     257  FROM orders o
     258)
     259SELECT
     260    (date_trunc('day', p.created_at))::date AS day,
     261    oc.channel,
     262    COUNT(DISTINCT p.order_id) AS paid_orders_cnt,
     263    SUM(p.amount)::numeric(14,2) AS revenue,
     264    SUM(p.tip_amount)::numeric(14,2) AS tip_total
     265FROM payments p
     266JOIN orders_channel oc ON oc.order_id = p.order_id
     267GROUP BY (date_trunc('day', p.created_at))::date, oc.channel;
     268}}}
     269When a user accesses the analytics page, the controller queries this simple, fast materialized view.
     270
     271{{{
     272
     273    @GetMapping("/analytics/by-channel")
     274    public String getPaymentsByChannel(Model model,
     275                                         @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
     276                                         @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
     277        LocalDate fromDate = (from == null) ? LocalDate.now().minusDays(30) : from;
     278        LocalDate toDate = (to == null) ? LocalDate.now() : to;
     279
     280        // This service method reads from the materialized view
     281        AnalyticsByChannelResponse response = analyticsService.getPaymentsDailyChannel(fromDate, toDate);
     282
     283        model.addAttribute("analyticsData", response);
     284        model.addAttribute("replaceTemplate", "analytics_dashboard");
     285        return "index";
     286    }
     287}}}
     288[[Image(analytics-1.png)]]
     289[[Image(monthly-revenue-split-orders.png)]]
     290
     291To keep the data in the materialized view up-to-date, a scheduled method is implemented in the application. This method runs every 30 minutes and executes the refresh command.
     292
     293
     294{{{
     295
     296    @Scheduled(cron = "0 */30 * * * *") // Runs every 30 minutes
     297    public void refreshPaymentAnalytics() {
     298        // This repository method executes a native query: "REFRESH MATERIALIZED VIEW mv_payments_daily_channel;"
     299        analyticsRepository.refreshDailyChannelView();
     300        log.info("Materialized view mv_payments_daily_channel has been refreshed.");
     301    }
     302}}}