Develop the API (CRUD Wallet, Transaction)

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.

Leave a Comment