Changes between Version 3 and Version 4 of ApplicationDevelopment


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

--

Legend:

Unmodified
Added
Removed
Modified
  • ApplicationDevelopment

    v3 v4  
    4141
    4242{{{
    43 @Override
    44 public Assignment createAssignment(CreateAssignmentDto dto, String managerEmail) {
    45 Manager manager = getManagerByEmail(managerEmail);
    46 Employee employee = employeeRepository.findById(dto.employeeId())
    47 .orElseThrow(() -> new EmployeeNotFoundException(dto.employeeId()));
    48 Shift shift = shiftRepository.findById(dto.shiftId())
    49 .orElseThrow(() -> new ShiftNotFoundException(dto.shiftId()));
    50 
    51     Assignment assignment = new Assignment(
    52             dto.clockInTime(),
    53             dto.clockOutTime(),
    54             manager,
    55             employee,
    56             shift
    57     );
    58 
    59     return assignmentRepository.save(assignment);
    60 }
     43    @Override
     44    public Assignment createAssignment(CreateAssignmentDto dto, String managerEmail) {
     45        Manager manager = getManagerByEmail(managerEmail);
     46        Employee employee = employeeRepository.findById(dto.employeeId())
     47                .orElseThrow(() -> new EmployeeNotFoundException(dto.employeeId()));
     48        Shift shift = shiftRepository.findById(dto.shiftId())
     49                .orElseThrow(() -> new ShiftNotFoundException(dto.shiftId()));
     50
     51        Assignment assignment = new Assignment(
     52                dto.clockInTime(),
     53                dto.clockOutTime(),
     54                manager,
     55                employee,
     56                shift
     57        );
     58
     59        return assignmentRepository.save(assignment);
     60    }
     61
    6162}}}
    6263The same implementation, translated into the SQL query that executes in the background, would look like this:
     
    8586[[Image(create-product.png)]]
    8687{{{
    87 @Operation(summary = "Create new product")
    88 @PostMapping("/add")
    89 public ResponseEntity<ProductDto> save(@RequestBody CreateProductDto dto) {
    90 return ResponseEntity.status(HttpStatus.CREATED).body(ProductDto.from(productService.createProduct(dto)));
    91 }
     88 @Operation(summary = "Create new product")
     89    @PostMapping("/add")
     90    public ResponseEntity<ProductDto> save(@RequestBody CreateProductDto dto) {
     91        return ResponseEntity.status(HttpStatus.CREATED).body(ProductDto.from(productService.createProduct(dto)));
     92    }
    9293}}}
    9394The service logic in ProductServiceImpl is responsible for creating the Product entity. A key feature is the conditional creation of an associated Inventory record. If the manageInventory flag is set to true, a new inventory entry is created in the same transaction. This ensures that a product meant to have its stock tracked will always have an inventory record from the moment of its creation.
    9495
    9596{{{
    96 @Override
    97 public Product createProduct(CreateProductDto dto) {
    98 Product productTmp = new Product();
    99 if (dto.name() != null) {
    100 productTmp.setName(dto.name());
    101 }
    102 if (dto.price() != null) {
    103 productTmp.setPrice(dto.price());
    104 }
    105 if(dto.taxClass()!=null){
    106 productTmp.setTaxClass(dto.taxClass());
    107 }
    108 
    109     productTmp.setCategory(categoryService.findById(dto.categoryId()));
    110     productTmp.setDescription(dto.description());
    111 
    112     if(dto.manageInventory()!=null){
    113         productTmp.setManageInventory(dto.manageInventory());
    114     }
    115     Product product = productRepository.save(productTmp);
    116     if(product.getManageInventory() == Boolean.TRUE){
    117         Inventory inventory = new Inventory(product, dto.quantity(), dto.restockLevel());
    118         inventoryRepository.save(inventory);
    119     }
    120     return product;
    121 }
     97    @Override
     98    public Product createProduct(CreateProductDto dto) {
     99        Product productTmp=new Product();
     100        if (dto.name() != null) {
     101            productTmp.setName(dto.name());
     102        }
     103        if (dto.price() != null) {
     104            productTmp.setPrice(dto.price());
     105        }
     106        if(dto.taxClass()!=null){
     107            productTmp.setTaxClass(dto.taxClass());
     108        }
     109
     110        productTmp.setCategory(categoryService.findById(dto.categoryId()));
     111        productTmp.setDescription(dto.description());
     112
     113        if(dto.manageInventory()!=null){
     114            productTmp.setManageInventory(dto.manageInventory());
     115        }
     116        Product product=productRepository.save(productTmp);
     117        if(product.getManageInventory()==Boolean.TRUE){
     118            Inventory inventory = new Inventory(product, dto.quantity(), dto.restockLevel());
     119            inventoryRepository.save(inventory);
     120        }
     121        return product;
     122    }
     123
    122124}}}
    123125The equivalent transactional SQL block for creating a product with inventory tracking would be:
     
    142144
    143145{{{
    144 @Operation(summary = "Create a new tab order for a logged-in front staff member")
    145 @PostMapping("/tab")
    146 public ResponseEntity<OrderDto> createTabOrder(@RequestBody CreateOrderDto dto, Authentication authentication) {
    147 String userEmail = authentication.getName();
    148 return ResponseEntity.ok(OrderDto.from(orderService.createTabOrder(dto, userEmail)));
    149 }
     146    @Operation(summary = "Create a new tab order for a logged-in front staff member")
     147    @PostMapping("/tab")
     148    public ResponseEntity<OrderDto> createTabOrder(@RequestBody CreateOrderDto dto, Authentication authentication) {
     149        String userEmail = authentication.getName();
     150        return ResponseEntity.ok(OrderDto.from(orderService.createTabOrder(dto, userEmail)));
     151    }
     152
    150153}}}
    151154[[Image(open-orders.png)]]
     
    155158
    156159{{{
    157 @Override
    158 @Transactional
    159 @CheckOnDuty
    160 public TabOrder createTabOrder(CreateOrderDto dto, String userEmail) {
    161 log.debug("User {} creating a tab order for table {}", userEmail, dto.tableNumber());
    162 User user = userRepository.findByEmail(userEmail)
    163 .orElseThrow(() -> new UsernameNotFoundException("User with email " + userEmail + " not found."));
    164 if (!(user instanceof FrontStaff)) {
    165 throw new SecurityException("User is not authorized to create tab orders.");
    166 }
    167 TabOrder tabOrder = new TabOrder();
    168 RestaurantTable table = tableRepository.findById(dto.tableNumber())
    169 .orElseThrow(() -> new TableNotFoundException(dto.tableNumber()));
    170 tabOrder.setRestaurantTable(table);
    171 tabOrder.setFrontStaff((FrontStaff) user);
    172 tabOrder.setTimestamp(LocalDateTime.now());
    173 tabOrder.setStatus(dto.status());
    174 if (dto.orderItems() != null && !dto.orderItems().isEmpty()) {
    175 List<OrderItem> orderItems = dto.orderItems().stream().map(itemDto -> {
    176 OrderItem item = new OrderItem();
    177 item.setOrder(tabOrder);
    178 // ... set other item properties
    179 Product product = productRepository.findById(itemDto.productId())
    180 .orElseThrow(() -> new ProductNotFoundException(itemDto.productId()));
    181 item.setProduct(product);
    182 return item;
    183 }).collect(Collectors.toList());
    184 tabOrder.setOrderItems(orderItems);
    185 }
    186 return tabOrderRepository.save(tabOrder);
    187 }
     160    @Override
     161    @Transactional
     162    @CheckOnDuty
     163    public TabOrder createTabOrder(CreateOrderDto dto, String userEmail) {
     164        log.debug("User {} creating a tab order for table {}", userEmail, dto.tableNumber());
     165        User user = userRepository.findByEmail(userEmail)
     166                .orElseThrow(() -> new UsernameNotFoundException("User with email " + userEmail + " not found."));
     167        if (!(user instanceof FrontStaff)) {
     168            throw new SecurityException("User is not authorized to create tab orders.");
     169        }
     170        TabOrder tabOrder = new TabOrder();
     171        RestaurantTable table = tableRepository.findById(dto.tableNumber())
     172                .orElseThrow(() -> new TableNotFoundException(dto.tableNumber()));
     173        tabOrder.setRestaurantTable(table);
     174        tabOrder.setFrontStaff((FrontStaff) user);
     175        tabOrder.setTimestamp(LocalDateTime.now());
     176        tabOrder.setStatus(dto.status());
     177        if (dto.orderItems() != null && !dto.orderItems().isEmpty()) {
     178            log.debug("OrderItems is not empty, processing items...");
     179            List<OrderItem> orderItems = dto.orderItems().stream().map(itemDto -> {
     180                OrderItem item = new OrderItem();
     181                item.setOrder(tabOrder);
     182                item.setQuantity(itemDto.quantity());
     183                item.setPrice(itemDto.price());
     184                item.setIsProcessed(itemDto.isProcessed());
     185                item.setTimestamp(LocalDateTime.now());
     186                Product product = productRepository.findById(itemDto.productId())
     187                        .orElseThrow(() -> new ProductNotFoundException(itemDto.productId()));
     188                item.setProduct(product);
     189                return item;
     190            }).collect(Collectors.toList());
     191            tabOrder.setOrderItems(orderItems);
     192        }
     193        return tabOrderRepository.save(tabOrder);
     194    }
     195
    188196}}}
    189197The corresponding SQL operations for creating a new tab order and adding an item would be:
     
    221229
    222230{{{
    223 @Override
    224 @Transactional
    225 @CheckOnDuty
    226 public Payment createPayment(CreatePaymentDto dto) {
    227 log.info("Creating payment for orderId: {}", dto.orderId());
    228 Order order = orderRepository.findById(dto.orderId())
    229 .orElseThrow(() -> new OrderNotFoundException(dto.orderId()));
    230 
    231     Payment payment = new Payment();
    232     payment.setAmount(dto.amount());
    233     payment.setTipAmount(dto.tipAmount());
    234     payment.setPaymentType(dto.paymentType());
    235     payment.setTimestamp(LocalDateTime.now());
    236     payment.setOrder(order);
    237 
    238     Payment saved = paymentRepository.save(payment);
    239 
    240     // Schedule MV refresh after the transaction commits
    241     mvRefresher.refreshPaymentsMvAfterCommit();
    242 
    243     return saved;
    244 }
     231    @Override
     232    @Transactional
     233    @CheckOnDuty
     234    public Payment createPayment(CreatePaymentDto dto) {
     235        log.info("Creating payment for orderId: {}", dto.orderId());
     236        Order order = orderRepository.findById(dto.orderId())
     237                .orElseThrow(() -> new OrderNotFoundException(dto.orderId()));
     238
     239        Payment payment = new Payment();
     240        payment.setAmount(dto.amount());
     241        payment.setTipAmount(dto.tipAmount());
     242        payment.setPaymentType(dto.paymentType());
     243        payment.setTimestamp(LocalDateTime.now()); // ensure mapped to created_at column
     244        payment.setOrder(order);
     245
     246        Payment saved = paymentRepository.save(payment);
     247
     248        // Schedule MV refresh after the transaction commits
     249        mvRefresher.refreshPaymentsMvAfterCommit();
     250
     251        return saved;
     252    }
     253
    245254}}}
    246255The trigger and the INSERT statement are defined in SQL as follows:
     
    267276[[Image(monthly-revenue-split-orders.png)]]
    268277{{{
    269 @GetMapping("/reveunueByChannel")
    270 public AnalyticsByChannelResponse paymentsDailyChannel(
    271 @RequestParam(required = false)
    272 @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
    273 @RequestParam(required = false)
    274 @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to
    275 ) {
    276 return analyticsService.getPaymentsDailyChannel(from, to);
    277 }
     278    @GetMapping("/reveunueByChannel")
     279    public AnalyticsByChannelResponse paymentsDailyChannel(
     280            @RequestParam(required = false)
     281            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
     282            @RequestParam(required = false)
     283            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to
     284    ) {
     285        return analyticsService.getPaymentsDailyChannel(from, to);
     286    }
     287
    278288}}}
    279289The service implementation fetches the pre-aggregated rows from the materialized view's repository (mvRepo) and then performs final calculations like totals and groupings in Java memory, which is extremely fast.
    280290
    281291{{{
    282 @Override
    283 public AnalyticsByChannelResponse getPaymentsDailyChannel(LocalDate from, LocalDate to) {
    284 if (from == null) from = LocalDate.now().minusDays(30);
    285 if (to == null)   to   = LocalDate.now();
    286 
    287     // pass null channel to fetch ALL channels (JPQL has :channel is null guard)
    288     List<PaymentsDailyChannel> rows = mvRepo.findRange(from, to, null);
    289 
    290     // group by channel
    291     Map<String, List<PaymentsDailyChannel>> byChannel = rows.stream()
    292             .collect(Collectors.groupingBy(PaymentsDailyChannel::getChannel));
    293 
    294     // ...further processing in Java to build the final DTO...
    295 
    296     return new AnalyticsByChannelResponse(from, to, channels, grandOrders, grandRevenue, grandTips);
    297 }
     292    @Override
     293    public AnalyticsByChannelResponse getPaymentsDailyChannel(LocalDate from, LocalDate to) {
     294        if (from == null) from = LocalDate.now().minusDays(30);
     295        if (to == null)   to   = LocalDate.now();
     296
     297        // pass null channel to fetch ALL channels (JPQL has :channel is null guard)
     298        List<PaymentsDailyChannel> rows = mvRepo.findRange(from, to, null);
     299
     300        // group by channel
     301        Map<String, List<PaymentsDailyChannel>> byChannel = rows.stream()
     302                .collect(Collectors.groupingBy(PaymentsDailyChannel::getChannel));
     303
     304        // build per-channel tables
     305        List<ChannelTableDto> channels = byChannel.entrySet().stream()
     306                .sorted(Map.Entry.comparingByKey()) // ONLINE, TAB, UNKNOWN (alphabetical)
     307                .map(e -> {
     308                    var data = e.getValue().stream()
     309                            .sorted(Comparator.comparing(PaymentsDailyChannel::getDay).thenComparing(PaymentsDailyChannel::getChannel))
     310                            .map(r -> new PaymentsDailyChannelDto(
     311                                    r.getDay(), r.getChannel(), r.getPaidOrdersCnt(), r.getRevenue(), r.getTipTotal()
     312                            ))
     313                            .toList();
     314
     315                    long totalOrders = e.getValue().stream()
     316                            .map(PaymentsDailyChannel::getPaidOrdersCnt)
     317                            .filter(v -> v != null)
     318                            .mapToLong(Long::longValue)
     319                            .sum();
     320
     321                    BigDecimal totalRevenue = e.getValue().stream()
     322                            .map(PaymentsDailyChannel::getRevenue)
     323                            .filter(v -> v != null)
     324                            .reduce(BigDecimal.ZERO, BigDecimal::add);
     325
     326                    BigDecimal totalTips = e.getValue().stream()
     327                            .map(PaymentsDailyChannel::getTipTotal)
     328                            .filter(v -> v != null)
     329                            .reduce(BigDecimal.ZERO, BigDecimal::add);
     330
     331                    return new ChannelTableDto(e.getKey(), data, totalOrders, totalRevenue, totalTips);
     332                })
     333                .toList();
     334
     335        // grand totals
     336        long grandOrders = rows.stream()
     337                .map(PaymentsDailyChannel::getPaidOrdersCnt)
     338                .filter(v -> v != null)
     339                .mapToLong(Long::longValue)
     340                .sum();
     341
     342        BigDecimal grandRevenue = rows.stream()
     343                .map(PaymentsDailyChannel::getRevenue)
     344                .filter(v -> v != null)
     345                .reduce(BigDecimal.ZERO, BigDecimal::add);
     346
     347        BigDecimal grandTips = rows.stream()
     348                .map(PaymentsDailyChannel::getTipTotal)
     349                .filter(v -> v != null)
     350                .reduce(BigDecimal.ZERO, BigDecimal::add);
     351
     352        return new AnalyticsByChannelResponse(from, to, channels, grandOrders, grandRevenue, grandTips);
     353    }}
    298354}}}
    299355The Materialized View definition in SQL is: