walletmanager

package module
v0.0.0-...-83fe226 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 10, 2022 License: GPL-3.0 Imports: 9 Imported by: 0

README

pipeline status coverage report Go Report Card

Wallet Manager

The Task

Write code for a Wallet App that behaves as follows:

  • User can deposit money into her wallet
  • User can withdraw money from her wallet
  • User can send money to another user
  • User can check her wallet balance

Other:

  • The Wallet App should keep track of all the users and wallets it contains
  • No UI interface is required, solution is provided as reusable library code
  • Submit your solution as a Zip file

Description

A library for depositing and withdrawing from a user's wallet, checking the balance of a wallet and transferring funds between users. This library is written in Go and requires a Postgres connection in order to retrieve Users, Wallets and Transactions from a database.

Usage

package main

import (
	"context"
	"log"

	"github.com/jackc/pgx/v4"
	"gitlab.com/mattshimwell/walletmanager"
)

const dummyUserID = "9843aa9a-61a4-41db-8622-9843aa9a4d4f"
const dummyDstUserID = "cfbbb1b8-8a28-4558-a1c6-cfbbb1b8d039"

func main() {
	ctx := context.Background()

	conn, err := pgx.Connect(ctx, "postgresql://root:root@localhost:5432/test")
	if err != nil {
		log.Fatalf("could not connect: %s", err.Error())
	}

	// Instantiate a new wallet manager
	mgr := walletmanager.New(conn)

	// List all wallets for a user
	wals, err := mgr.List(ctx, dummyUserID)
	if err != nil {
		log.Fatalf("error listing user wallets: %s", err.Error())
	}

	wal := wals[0]

	// Check a wallet's balance
	bal, err := mgr.Balance(ctx, wal.ID)
	if err != nil {
		log.Fatalf("error retrieving balance: %s", err.Error())
	}
	log.Println(bal)

	// Make a deposit
	err = mgr.Deposit(ctx, wal.ID, float64(100))
	if err != nil {
		log.Fatalf("error making deposit: %s", err.Error())
	}

	// Make a withdrawal
	err = mgr.Withdraw(ctx, wal.ID, float64(100))
	if err != nil {
		log.Fatalf("error making withdrawal: %s", err.Error())
	}

	// List all wallets for a user
	newWals, err := mgr.List(ctx, dummyDstUserID)
	if err != nil {
		log.Fatalf("error listing user wallets: %s", err.Error())
	}

	newWal := newWals[0]

	// Transfer from one wallet to another
	err = mgr.Transfer(ctx, wal.ID, newWal.ID, float64(100))
	if err != nil {
		log.Fatalf("error making transfer: %s", err.Error())
	}
}

Tests

A full suite of unit and integration tests are provided:

  • To run unit tests (requires Go): make race.
  • To run integration tests (requires Docker + Compose): make up && make integration.

Test coverage: coverage report

CI

GitLab CI has been added for this project, running Go tools and tests.

Decisions Made

  • Given that the requirements state that this solution should be provided as "reusable library code", no standalone API/RPC is included.
    • I sought further clarification on what exactly was meant by "reusable library code", and whether that meant that an API was not in scope for this task. I was told that an API or standalone microservice was indeed also acceptable. With more time allocated to this task I would therefore add this in a further iteration (see improvements).
  • Postgres was chosen as data store due to its relational nature, ease of setup and familiarity. Replacing this data store with something else would be easy enough considering the separation between the storage and service layers of the library.
    • The assumption is therefore made that the code implementing this library will have access to a Postgres database.
    • Given that the requirements state "The Wallet App should keep track of all the users and wallets it contains" I think this is a fair assumption.
    • As an alternative the library could provide a less opinionated database.Interface which would then allow for other storage engines to be used.
  • The act of depositing and withdrawing refer specifically to incrementing and decrementing the Balance of a user's Wallet. In a production setting I would therefore expect this tool to either be integrated with or produce events for a software component that would actually facilitate the moving of money from one location/form to another.
  • A List wallets by user method has also been added as an addition criteria that was given, after requesting clarification on "The Wallet App should keep track of all the users and wallets it contains".
  • Migrations have been written for this database for the purpose of integration testing.
  • Users own one or more Wallet, in which the current balance of each wallet is stored.
  • Transactions are separate entities that are owned by Wallets and detail the type of transaction made (credit, debit, transfer) as well as the amount and timestamp.
  • In the case of (internal) transfers between wallets, both the source and destination wallet ID are recorded in the corresponding Transaction.
  • Transfers between wallets are independent of the users that own them, so functionally there is no difference between transferring between wallets belonging to the same or different users. This means that the same method can be used for transferring between users or between a single user's wallets.

Reviewing

  • All efforts have been made to write and layout the code in as clean and idiomatic style as possible, whilst taking care not to "over-optimise" given the time allocated to complete the task.
  • Interfaces are used to allow for effective mocking and testing, but are not overused. The code follows "standard" idioms and structure, per CodeReviewComments and Effective Go.
  • The Manager methods provided attempt to represent a DDD approach.
  • To that end, the directory layout attempts to be as "vertical" as practical, whilst still maintaining separations across files and packages.
  • Dependency injection is handled sensibly, requiring the user to provide a struct that satisfies database.Interface to walletmanager.Manager, which is in turn then used to instantiate the wallet and transaction stores.
  • All exported functions are commented to ensure the code is well-documented.
  • Lastly, the main walletmanager.Manager struct presents a clear and obvious usage, ready to be included in whatever API/service/component this would eventually be included in when imported into a production project.

Improvements

  • Model Users in-code rather than just in the database and add some more user-related methods. Currently they're only used when listing wallets.
  • Modelling balances/transaction values as float64 is probably overkill and requires some Math operations to address rounding errors. Depending on the type of currency being dealt with, such values could be modelled as simple int.
  • Allow for more CRUD operations on Wallets. Currently Wallets cannot be created or destroyed, only updated
  • Wallet.Balance is the current source of truth for a current balance, and should be exactly the sum of Transactions for that user, however no such reconciliation is ever performed. It probably should be in order to keep the balance accurate
  • Build out a gRPC server for this library so that it can stand alone (not in scope)
    • Containerise this gRPC server
    • Add some deployment tooling (k8s, Helm, CD pipeline)
  • Better handling of SQL errors (not found, duplicate, key constraint violations, etc)
    • To that end may be helpful to provide specific error types with custom fields
  • Add integration tests to CI
  • Add a lot more fields on Transactions (status, more types, etc.)

Time Spent

~4 hours

Data Structure

Feedback

Overall this has been an interesting exercise, with plenty of opportunity to demonstrate a variety of different programming disciplines. If I could offer some feedback:

  • It may be helpful to give some more context on the entities described in the requirements. What's a User? What's a Wallet? When we say Deposit, what do we mean? Such Ubiqitous Language makes it much easier to understand the task at hand.
  • The requirements state that submissions should be provided as "library code", but after having sought further clarification I was also advised that providing an API or a standalone program were also acceptable. It may be helpful to detail this in the requirements, and adjust the desired time spent on the task accordingly.
  • Further to that, some Given-When-Then (or alternative BDD format) acceptance tests would be very useful in allowing a candidate to get right into the task at hand, without worrying whether they've misunderstood what they're supposed to be doing. We work with stories in tickets all day long, so the closer such a task's description is to that format, the easier it is to understand.
  • If an API is indeed acceptable, perhaps some contract tests could be provided?

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager provides actions to perform on a wallet:

List: list all wallets for a given user.
Balance: view the current balance of a wallet by ID.
Deposit: add funds to a wallet.
Withdraw: withdraw funds from a wallet.
Transfer: transfers funds from one wallet to another.

func New

func New(db database.Interface) Manager

New returns a new Manager.

func (Manager) Balance

func (m Manager) Balance(ctx context.Context, walletID string) (float64, error)

Balance view the current balance of a wallet by ID.

func (Manager) Deposit

func (m Manager) Deposit(ctx context.Context, walletID string, amount float64) error

Deposit add funds to a wallet.

func (Manager) List

func (m Manager) List(ctx context.Context, userID string) ([]wallet.Wallet, error)

List all wallets by a given user ID.

func (Manager) Transfer

func (m Manager) Transfer(ctx context.Context, srcWalletID, dstWalletID string, amount float64) error

Transfer funds internally, from one wallet to another.

func (Manager) Withdraw

func (m Manager) Withdraw(ctx context.Context, walletID string, amount float64) error

Withdraw funds from a wallet.

type TransactionStore

type TransactionStore interface {
	Create(ctx context.Context, tx pgx.Tx, txn transaction.Transaction) error
}

TransactionStore provides methods for creating transatction records in storage.

type WalletStore

type WalletStore interface {
	List(ctx context.Context, userID string) ([]wallet.Wallet, error)
	Get(ctx context.Context, walletID string) (wallet.Wallet, error)
	Update(ctx context.Context, tx pgx.Tx, wallet wallet.Wallet) error
}

WalletStore provides methods for getting and updating wallet records in storage.

Directories

Path Synopsis
test
mocks/database
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.
mocks/manager
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL