Table of Contents
Overview
With all the setup done, let’s focus on making our app. The main purpose of the app is to let the user track their income and expenses. You’ve probably come through apps like this many times. We can spend the whole day listing all possible features an app like this can have. However, for simplicity’s sake, let’s keep the app simple.
The Application’s requirements
Here are the main functionalities of our app:
- A user can have multiple wallets
- A user can do CRU(D) on wallets that he creates
- In a wallet, a user can create income and expenses
- A user cannot delete an expense or income (once recorded, it cannot be deleted, like in the blockchain)
- A user can add images to income/expenses (bills, for example)
- A user can share a wallet so other people can see or add their own expenses to the wallet (think of a family wallet – this one is cool but optional)
That’s pretty much it.
From these requirements, we can come up with the models
The application’s models
It’s quite obvious that we need to have the following models:
@Document(collection = "wallets") @Data public class Wallet { @MongoId private String id; private String ownerId; private String title; private String description; private long balance; private String currency; @CreatedDate private LocalDateTime createdDate; }
This is the Wallet model. Usually, the wallet needs to have an owner, the currency, and the title and description.
Next, let’s create the Transaction model:
@Document(collection = "transactions") @Data public class Transaction { @MongoId private String id; private String walletId; private String title; private String description; private String category; private TransactionType type; private List<String> images = List.of();//object ids of images private long amount; private String currency; private String ownerId; private LocalDateTime createdDate; }
Transaction type:
public enum TransactionType { DEPOSIT, WITHDRAW }
The fields of the Transaction are quite self-explanatory. The images are of a list because we will upload the images to an object storage service such as S3 and get the object ID back.
With the models ready, let’s create the repositories and services.
Wallet Repository
The WalletRepository extends MongoRepository which has quite some useful default methods. We only need to add the missing ones:
@Repository public interface WalletRepository extends MongoRepository<Wallet, String> { Optional<Wallet> findByOwnerIdAndId(String ownerId, String walletId); Page<Wallet> findAllByOwnerId(String ownerId, Pageable page); }
WalletService
The WalletService handles the CRUD operation of wallets:
@Service @RequiredArgsConstructor public class WalletService { private final WalletRepository walletRepository; private final CurrentUserService currentUserService; public Wallet create(Wallet wallet) { //assert that wallet has a title if (wallet.getTitle() == null || wallet.getTitle().isEmpty()) { throw new IllegalArgumentException("Wallet title is required"); } //set default currency if not provided if (wallet.getCurrency() == null || wallet.getCurrency().isEmpty()) { wallet.setCurrency("USD"); } wallet.setOwnerId(currentUserService.getCurrentUserId()); return walletRepository.save(wallet); } public Wallet update(Wallet wallet) { //assert that wallet has a title if (wallet.getTitle() == null || wallet.getTitle().isEmpty()) { throw new IllegalArgumentException("Wallet title is required"); } walletRepository.findByOwnerIdAndId(currentUserService.getCurrentUserId(), wallet.getId()) .orElseThrow(() -> new IllegalArgumentException("Wallet not found")); return walletRepository.save(wallet); } public Page<Wallet> list(int page, int size) { return walletRepository.findAllByOwnerId(currentUserService.getCurrentUserId(), PageRequest.of(page, size)); } public Wallet get(String walletId) { return walletRepository.findByOwnerIdAndId(currentUserService.getCurrentUserId(), walletId) .orElseThrow(() -> new IllegalArgumentException("Wallet not found")); } public void delete(String walletId) { walletRepository.findByOwnerIdAndId(currentUserService.getCurrentUserId(), walletId) .orElseThrow(() -> new IllegalArgumentException("Wallet not found")); walletRepository.deleteById(walletId); } }
You noticed that I have a service called CurrentUserService. That’s a utility service that extracts the user id from Auth0 token:
@Service public class CurrentUserService { public String getCurrentUserId() { var authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication.isAuthenticated()) return authentication.getName(); throw new IllegalArgumentException("User not authenticated"); } }
Finally, let’s create the controller so our Angular app can call APIs to do CRUD operations on wallets:
@RestController @RequestMapping("/api/wallets") @RequiredArgsConstructor public class WalletController { private final WalletService walletService; @GetMapping("/{id}") public Wallet get(@PathVariable String id) { return walletService.get(id); } @GetMapping public Page<Wallet> list( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "10") int size ) { return walletService.list(page, size); } @PostMapping public Wallet create(@RequestBody Wallet wallet) { return walletService.create(wallet); } @PutMapping public Wallet update(@RequestBody Wallet wallet) { return walletService.update(wallet); } @DeleteMapping("/{id}") public void delete(@PathVariable String id) { walletService.delete(id); } }
Now we are done with the wallet CRUD, let’s continue with the transaction’s CRUD.
TransactionRepository
TransactionRepository also extends MongoRepository and only requires some basic functions:
@Repository public interface TransactionRepository extends MongoRepository<Transaction, String> { Page<Transaction> findAllByWalletId(String walletId, Pageable page); Page<Transaction> findAllByWalletIdAndType(String walletId, TransactionType type, Pageable page); }
TransactionService
TransactionService handles CRUD for transaction, as mentioned, we don’t let user delete transaction.
@Service @RequiredArgsConstructor public class TransactionService { private final TransactionRepository transactionRepository; private final CurrentUserService currentUserService; private final WalletRepository walletRepository; public Transaction create(Transaction transaction) { validateTransaction(transaction); return transactionRepository.save(transaction); } public Transaction update(Transaction transaction) { //make sure wallet exists validateTransaction(transaction); return transactionRepository.save(transaction); } public PageResponse<Transaction> list(String walletId, int page, int size) { var result = transactionRepository.findAllByWalletId(walletId, PageRequest.of(page, size)); return PageResponse.<Transaction>builder() .data(result.getContent()) .page(result.getNumber()) .size(result.getSize()) .totalPages(result.getTotalPages()) .totalElements(result.getTotalElements()) .build(); } private void validateTransaction(Transaction transaction) { walletRepository.findByOwnerIdAndId(currentUserService.getCurrentUserId(), transaction.getWalletId()).orElseThrow(() -> new IllegalArgumentException("Wallet not found")); if (transaction.getType() == null) { throw new IllegalArgumentException("Transaction type is required"); } if (transaction.getTitle() == null) { throw new IllegalArgumentException("Transaction title is required"); } } }
TransactionController
Finally, we create TransactionController so the Angular app can call the CRUD API:
@RestController @RequestMapping("/api/transactions") @RequiredArgsConstructor public class TransactionController { private final TransactionService transactionService; @PostMapping public Transaction create(@RequestBody Transaction transaction) { return transactionService.create(transaction); } @PutMapping public Transaction update(@RequestBody Transaction transaction) { return transactionService.update(transaction); } @GetMapping("/wallet/{walletId}") public PageResponse<Transaction> list( @PathVariable String walletId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "10") int size ) { return transactionService.list(walletId, page, size); } }
Conclusion
In this post, we’ve created the API to do CRUD operations on wallets and transactions. There is one task left on the API side, that’s image upload. In the next post, we are going to create an API to upload images to AWS S3.
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.