Skip to content

Build fintech application with Spring Boot, Vavr and Vue.js (Part 4 – Invoices and bills backend)

Hello! Let me present part 4 of my tutorial series “Build fintech application using Spring Boot, Vue.js and Vavr”. This time we will implement backend API for invoices and bills – fundamentals for any accounting app! We would refresh topics, we already talked about, and also talk about new stuff, for instance about server side validation in Spring Framework.

Table of contents

What we will do in this part

In this post we will repeat concepts, that we already discussed in the part 2: Spring components, entities etc. However we will also add new elements – server-side validation and how to work with sets in Spring Data and Vavr.

Step 1. Do entities

In this part we have 3 type of entities: Invoice, Bill and Item. First two represent documents and are same, however it is a good idea to define them in separate data models, so later you can change their structure appropriately. The last one represent the document’s item. Let define documents first.

Step 1.1 Documents definition

Invoice.java

@Value
@AllArgsConstructor
@Document (collection="invoices")
public class Invoice {

    @Id @NonFinal String invoiceId;
    String userId;
    String customerId;
    Date issuedDate;
    Date dueDate;
    BigDecimal subtotal;
    BigDecimal total;
    double tax;
    Set<Item> items;
}

Bill.java

@Value
@AllArgsConstructor
@Document (collection="bills")
public class Bill {

    @Id @NonFinal String billId;
    String userId;
    String vendorId;
    Date issuedDate;
    Date dueDate;
    BigDecimal subtotal;
    BigDecimal total;
    double tax;
    Set<Item> items;
}

What is worth to note here:

  • Here we use BigDecimal objects to keep subtotal and total amounts
  • Tax amount is represented by a double value from 0.0 to 1.0. In our tutorial we will do client-side calculations, so we need to check that total and subtotal values that came to server are not null – we will move there shortly.
  • We keep dates in form of milliseconds using java.util.Date

Next part is to define items

Step 1.2. Items

Generally document item has following data: position, description, rate and quantity. You remember that we use Vavr Set to keep items – that means that items should be unique. Sets use equals() method to determine an uniqueness of entity; in other words, for unique element e1 is true that there is no element e2 in set already that e1.equals(e2) == true. Lombok overrides the equals method as part of @Value annotation, but we can customize its behavior and include or exclude fields that we want to use. Another good idea is to implement Comparable interface that is used for sorting of set – we sort using item’s position, therefore we need to compare this value. Take a look on the code snippet below, that represents document’s item:

@Value
@AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Item implements Comparable<Item>{
    @EqualsAndHashCode.Include int position;
    String description;
    BigDecimal rate;
    long quantity;

    @Override
    public int compareTo(Item o) {
        return Integer.valueOf(this.position)
        		.compareTo(Integer.valueOf(o.getPosition()));
    }
}

Note:

  • @EqualsAndHashCode(onlyExplicitlyIncluded = true) annotation tells to Lombok, that generated version of equals() method should include only such fields, we explicitly annotated with @EqualsAndHashCode.Include
  • We implemented Comparable interface, that allows us to sort items based on position (we use native compareTo method from Integer class)

Step 1.3. Server side validation

Another important concept to tackle in this part is validation. From a technical point of view, it stands for a process that ensures that the user provided necessary and properly formatted information needed to successfully complete an operation.. We perform both client-side and server-side validation in this project, but in this part we focus on server-side validation.

Spring offers us handy tools to do this task. First, let annotate fields, that are mandatory using @NonNull annotation:

Bill.java

@Value
@AllArgsConstructor
@Document (collection="bills")
public class Bill {
    @Id @NonFinal String billId;
    @NonNull String userId;
    @NonNull String vendorId;
    Date issuedDate;
    Date dueDate;
    @NonNull BigDecimal subtotal;
    @NonNull BigDecimal total;
    double tax;
    Set<Item> items;
}

Invoice.java

@Value
@AllArgsConstructor
@Document (collection="invoices")
public class Invoice {

    @Id @NonFinal String invoiceId;
    @NonNull String userId;
    @NonNull String customerId;
    Date issuedDate;
    Date dueDate;
    @NonNull BigDecimal subtotal;
    @NonNull BigDecimal total;
    double tax;
    Set<Item> items;

}

Now, if required data is not provided (e.g. annotated fields have null values), Spring throws NullPointerException. Let test this behavior and write some unit tests:

BillNonNullTest.java

public class BillNonNullTest {

    @Test
    public void validateNonNullTest(){
        assertThrows(NullPointerException.class, () -> {
            Bill bill = new Bill(null, null, null, 
                        new Date(), new Date(), BigDecimal.ONE, BigDecimal.ONE, 0 , null);
            System.out.println("Validation failed: " +bill.toString());
        });
    }
}

InvoiceNonNullTest.java

public class InvoiceNonNullTest {

    @Test
    public void validateNonNullTest(){
        assertThrows(NullPointerException.class, () -> {
            Invoice invoice = new Invoice(null, null, null, 
                    new Date(), new Date(), BigDecimal.ONE, BigDecimal.ONE, 0 , null);
            System.out.println("Validation failed: " +invoice.toString());
        });
    }
}

Run these tests to ensure that Spring does validation:

Image 1. Non null tests results

Next step is to do repositories.

Step 2. Create repositories

In the 2nd part we already talked that repositories are responsible for work with data sources and abstract all underlaying implementations, so we can concentrate on business logic. As Spring supplies us CrudRepository that has most common operations already built-in, we can just extend it. However, as we use Vavr Options and immutable lists, we need to explicitly provide methods, that return following objects.

IInvoiceRepository.java

public interface IInvoiceRepository 
extends CrudRepository<Invoice, String> {

    Option<Invoice> findByInvoiceId (String invoiceId);

    List<Invoice> findByUserId (String userId);

}

IBillRepository.java

public interface IBillRepository 
extends CrudRepository<Bill, String> {

    Option<Bill> findByBillId (String billId);

    List<Bill> findByUserId (String userId);
}

Now we can use these repositories in services.

Step 3. Create services

From a technical point of view, service is a component, that contains actual business logic. In the 2nd part we already agreed to use test-driven development approach to build services. That means, that we first define service contract and write a test and then do an actual implementation.

Step 3.1. Define interfaces

First step here is to define services’ contracts:

IInvoiceService.java

public interface IInvoiceService {

    Invoice createInvoice (Invoice invoice);

    void removeInvoice (String id);

    Option<Invoice> findInvoiceById (String id);

    List<Invoice> findForUser (String userId);
}

IBillService.java

public interface IBillService {

    Bill createBill (Bill bill);

    void removeBill (String billId);

    Option<Bill> findBillById (String id);

    List<Bill> findBillsForUser (String userId);
}

Also, let implement these services by InvoiceServiceImpl.java and BillServiceImpl.java classes respectively without actual logic. Now we need to write tests for these classes.

Step 3.2. Write tests

To ensure that services perform as expected, we do unit tests. As in the previous part, we use JUnit to write tests for Spring components. Here are code snippets for both service tests:

InvoiceServiceImplTest.java

@ExtendWith(MockitoExtension.class)
public class InvoiceServiceImplTest {

    @Mock private IInvoiceRepository repository;
    @InjectMocks private InvoiceServiceImpl service;

    private Invoice createMockInvoice (String invoiceId, String userId){
        return new Invoice(invoiceId, userId, "companyId", 
            new Date(), new Date(), BigDecimal.ONE, BigDecimal.TEN, 0.25, null);
    }

    @Test
    public void createInvoiceTest (){
        Invoice data = createMockInvoice("12345678910", "yuri");
        when(repository.save(data)).thenReturn(data);
        Invoice result = service.createInvoice(data);

        assertEquals(data.getInvoiceId(), result.getInvoiceId());
        assertEquals(data.getUserId(), result.getUserId());
    }

    @Test
    public void findInvoiceSuccessTest(){
        String invoiceId = "12345678910";
        Invoice data = createMockInvoice(invoiceId, "yuri");
        when(repository.findByInvoiceId(invoiceId)).thenReturn(Option.of(data));

        Option<Invoice> result = service.findInvoiceById(invoiceId);
        assertTrue(result.isDefined());

    }

    @Test
    public void findInvoiceFailedTest(){
        String invoiceId = "987654321";
        when(repository.findByInvoiceId(invoiceId)).thenReturn(Option.none());

        Option<Invoice> result = service.findInvoiceById(invoiceId);
        assertTrue(result.isEmpty());
    }
}

BillServiceImplTest.java

@ExtendWith(MockitoExtension.class)
public class BillServiceImplTest {

    @Mock private IBillRepository repository;
    @InjectMocks private BillServiceImpl service;

    private Bill createMockBill (String billId, String userId){
        return new Bill(billId, userId, "companyId", new Date(), new Date(), BigDecimal.ONE, BigDecimal.TEN, 0.25, null);
    }

    @Test
    public void createBillTest (){
        Bill data = createMockBill("123456789", "yuri");
        when(repository.save(data)).thenReturn(data);

        Bill result = service.createBill(data);
        assertEquals(data.getBillId(), result.getBillId());
        assertEquals(data.getUserId(), result.getUserId());
    }

    @Test
    public void findBillSuccessTest(){
        String billId = "123456789";
        Bill data = createMockBill(billId, "yuri");
        when(repository.findByBillId(billId)).thenReturn(Option.of(data));

        Option<Bill> result = service.findBillById(billId);
        assertTrue(result.isDefined());
    }

    @Test
    public void findBillFailedTest(){
        String billId = "987654321";
        when(repository.findByBillId(billId)).thenReturn(Option.none());

        Option<Bill> result = service.findBillById(billId);
        assertTrue(result.isEmpty());
    }
}

Note following points:

  • We use helper methods createMockInvoice and createMockBill to create some mock data
  • We validate presence or absence of data in Option using isDefined and isEmpty methods

Step 3.3. Implement services

Next step is to provide actual implementations of services. We already did it, so there is nothing new in this part.

InvoiceServiceImpl.java

@Component("IInvoiceService")
public class InvoiceServiceImpl implements IInvoiceService {

    @Autowired
    private IInvoiceRepository repository;

    @Override
    public Invoice createInvoice(Invoice invoice) {
        return repository.save(invoice);
    }

    @Override
    public void removeInvoice(String id) {
        repository.deleteById(id);
    }

    @Override
    public Option<Invoice> findInvoiceById(String id) {
        return repository.findByInvoiceId(id);
    }

    @Override
    public List<Invoice> findForUser(String userId) {
        return repository.findByUserId(userId);
    }

}

BillServiceImpl.java

@Component("IBillService")
public class BillServiceImpl implements IBillService {

    @Autowired
    private IBillRepository repository;

    @Override
    public Bill createBill(Bill bill) {
        return repository.save(bill);
    }

    @Override
    public void removeBill(String id) {
        repository.deleteById(id);
    }

    @Override
    public Option<Bill> findBillById(String id) {
        return repository.findByBillId(id);
    }

    @Override
    public List<Bill> findBillsForUser(String userId) {
        return repository.findByUserId(userId);
    }

}

Run tests now to validate that services behave as expected:

Image 2. Service tests results

Final thing here are controllers.

Step 4. REST controllers

If you’ve read the part 2 you will already know what we will do here. There is nothing new, except couple of things. Here are implementations of REST API controllers:

BillRestController.java

@RestController
@RequestMapping("/v1/bills")
@CrossOrigin(origins = "*")
public class BillRestController {

    @Autowired
    private IBillService billService;

    @PostMapping("/")
    public ResponseEntity<Bill> createBill (@Valid @RequestBody Bill payload){
        Bill result = billService.createBill(payload);
        return ResponseEntity.ok(result);
    }

    @DeleteMapping("/{id}")
    public void removeBill (@PathVariable String id){
        billService.removeBill(id);
    }

    @GetMapping("/all/{userId}")
    public ResponseEntity<List<Bill>> findBillsForUser(@PathVariable String userId){
        List<Bill> results = billService.findBillsForUser(userId);
        return ResponseEntity.ok(results);
    }

    @GetMapping("/one/{id}")
    public ResponseEntity<Bill> findBillById (@PathVariable(name = "id") String billId){
        Option<Bill> result = billService.findBillById(billId);
        if (result.isDefined()){
            Bill data = result.get();
            return ResponseEntity.ok(data);
        }
        return ResponseEntity.notFound().build();
    }
}

InvoiceRestController.java

@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/v1/invoices")
public class InvoiceRestController {

    @Autowired 
    private IInvoiceService invoiceService;

    @PostMapping("/")
    public ResponseEntity<Invoice> createInvoice (@Valid @RequestBody Invoice payload){
        Invoice result = invoiceService.createInvoice(payload);
        return ResponseEntity.ok(result);
    }

    @DeleteMapping("/{invoiceId}")
    public void removeInvoice (@PathVariable String invoiceId){
        invoiceService.removeInvoice(invoiceId);
    }

    @GetMapping("/all/{userId}")
    public ResponseEntity<List<Invoice>> findInvoicesForUser (@PathVariable String userId){
        List<Invoice> results = invoiceService.findForUser(userId);
        return ResponseEntity.ok(results);
    }

    @GetMapping("/one/{id}")
    public ResponseEntity<Invoice> findInvoiceById (@PathVariable(name = "id") String invoiceId){
        Option<Invoice> result = invoiceService.findInvoiceById(invoiceId);
        if (result.isDefined()){
            Invoice data = result.get();
            return ResponseEntity.ok(data);
        }
        return ResponseEntity.notFound().build();
    }
}

What is important to note here:

  • We annotated RequestBody with @Valid annotation – this tells to Spring to do validation on rules, we did in step 1.3.
  • As path variables have different actual names with Java objects, we explicitly map them using @PathVariable(name = "id") annotations

Now open Postman and test controllers – I’ll take Bill controller as a candidate to make this post shorter. First, let add some bills:

Image 3. Add bill test

Now we can retrieve bills for user:

Image 4. Get bills for user test

In a same way, we can access one specific bill using its ID:

Image 5. Get bill by id test

We also need to ensure, that Spring performs validation in controllers. Here we will do it with invoices. You remember, that in step 1.3. we annotated most fields with @NonNull annotation. So, if we miss for example companyId, Spring will throw an exception:

Image 6. Missed data validation test

Otherwise, if data is correct, Spring adds it to database and return code 200:

Image 7. Valid data test

So, this is all for this part. In the next post we implement Vue part for invoices and bills, including client side validation, calculations and time operations. Meanwhile, follow me to be notfiied about updates!

Copy link
Powered by Social Snap