package main

import (
	"errors"
	"fmt"
)

// TxStore meniru tabel database berisi saldo rekening.
// Semua perubahan hanya terjadi lewat transaksi: Begin, Commit, atau Rollback.
type TxStore struct {
	saldo map[string]int
}

// Tx menampung perubahan sementara. Perubahan baru menyentuh
// store saat Commit dipanggil; Rollback membuang seluruhnya.
type Tx struct {
	store   *TxStore
	pending map[string]int
	selesai bool
}

// ErrTxDitutup dikembalikan bila transaksi dipakai setelah Commit atau Rollback.
var ErrTxDitutup = errors.New("transaksi sudah ditutup")

// Begin memulai transaksi dengan menyalin state store saat ini.
func (s *TxStore) Begin() *Tx {
	pending := make(map[string]int, len(s.saldo))
	for k, v := range s.saldo {
		pending[k] = v
	}
	return &Tx{store: s, pending: pending}
}

func (tx *Tx) Tarik(rekening string, jumlah int) error {
	if tx.selesai {
		return ErrTxDitutup
	}
	if jumlah > tx.pending[rekening] {
		return errors.New("saldo tidak cukup")
	}
	tx.pending[rekening] -= jumlah
	return nil
}

func (tx *Tx) Setor(rekening string, jumlah int) error {
	if tx.selesai {
		return ErrTxDitutup
	}
	tx.pending[rekening] += jumlah
	return nil
}

// Commit menerapkan seluruh perubahan pending ke store, lalu menutup transaksi.
func (tx *Tx) Commit() {
	tx.store.saldo = tx.pending
	tx.selesai = true
}

// Rollback membuang seluruh perubahan pending, lalu menutup transaksi.
func (tx *Tx) Rollback() {
	tx.selesai = true
}

func (s *TxStore) Saldo(rekening string) int {
	return s.saldo[rekening]
}

func main() {
	store := &TxStore{saldo: map[string]int{
		"Budi": 100000,
		"Siti": 50000,
	}}
	fmt.Printf("awal          : Budi=%d Siti=%d\n", store.Saldo("Budi"), store.Saldo("Siti"))

	// Transaksi 1: transfer sukses, ditutup dengan Commit.
	tx := store.Begin()
	if err := tx.Tarik("Budi", 30000); err != nil {
		tx.Rollback()
		fmt.Println("transaksi 1 gagal:", err)
		return
	}
	if err := tx.Setor("Siti", 30000); err != nil {
		tx.Rollback()
		fmt.Println("transaksi 1 gagal:", err)
		return
	}
	tx.Commit()
	fmt.Printf("setelah commit: Budi=%d Siti=%d\n", store.Saldo("Budi"), store.Saldo("Siti"))

	// Transaksi 2: penarikan melebihi saldo, gagal, ditutup dengan Rollback.
	tx2 := store.Begin()
	if err := tx2.Tarik("Siti", 999999); err != nil {
		fmt.Println("transaksi 2 gagal:", err)
		tx2.Rollback()
	}
	fmt.Printf("setelah rollback: Budi=%d Siti=%d\n", store.Saldo("Budi"), store.Saldo("Siti"))
}
