Skip to content

Commit

Permalink
chore: complete initial features
Browse files Browse the repository at this point in the history
  • Loading branch information
shanehull committed Sep 4, 2024
1 parent d6f8819 commit 143db20
Show file tree
Hide file tree
Showing 34 changed files with 1,348 additions and 373 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tailwindcss
tmp/
bin/
12 changes: 8 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
FROM golang:1.22-alpine as builder
FROM golang:1.22-alpine AS builder

WORKDIR /app

COPY . .

RUN apk update && apk upgrade && apk add --no-cache ca-certificates git
RUN apk update && apk upgrade && apk add --no-cache ca-certificates
RUN update-ca-certificates

RUN go install github.com/a-h/templ/cmd/templ@latest

ARG GIT_TAG="unknown"

RUN GO_ENABLED=0 GOOS=linux go build \
-ldflags "debtrecyclingcalc.com/internal/buildinfo.GitTag=$(git describe --tags)" \
-ldflags "-X debtrecyclingcalc.com/internal/buildinfo.GitTag=${GIT_TAG}"\
-o ./bin/main ./cmd/

FROM scratch

ENV ALLOWED_ORIGIN="*"
ENV SERVER_HOST="0.0.0.0"
ENV SERVER_HOST="127.0.0.1"

COPY --from=builder /app/bin/main ./bin/main
COPY --from=builder /app/static ./static

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080

CMD ["./bin/main"]
7 changes: 0 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,3 @@ dev:
make templ-generate
go build -ldflags "-X debtrecyclingcalc.com/internal/buildinfo.GitTag=dev" \
-o ./tmp/main ./cmd/ && air

.PHONY: build
build:
make tailwind-build
make templ-generate
go build -ldflags "debtrecyclingcalc.com/internal/buildinfo.GitTag=$(git describe --tags)" \
-o ./bin/main ./cmd/
61 changes: 40 additions & 21 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,80 @@ import (
"context"
"fmt"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"debtrecyclingcalc.com/internal/buildinfo"
"debtrecyclingcalc.com/internal/handlers"
"debtrecyclingcalc.com/internal/middleware"
)

var (
allowedOrigin = "*"
serverHost = "127.0.0.1"
serverPort = "8080"
allowedOrigin = "*"
serverHost = "127.0.0.1"
serverPort = "8080"
htmxHash = "sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
hyperscriptHash = "sha384-+Uth1QzYJsTjnS5SXVN3fFO4I32Y571xIuv53WJ2SA7y5/36tKU1VCutONAmg5eH"
echartsHash = "sha384-Mx5lkUEQPM1pOJCwFtUICyX45KNojXbkWdYhkKUKsbv391mavbfoAmONbzkgYPzR"
)

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx, cancel := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)

// Check if the SERVER_HOST env var is set and override
fmt.Println("gitTag", buildinfo.GitTag)

// If the SERVER_HOST env var is set, use that
envHost, ok := os.LookupEnv("SERVER_HOST")
if ok {
serverHost = envHost
}

// Check if SH_ALLOWED_ORIGIN env var is set and override
// If the ALLOWED_ORIGIN env var is set, use that
envOrigin, ok := os.LookupEnv("ALLOWED_ORIGIN")
if ok {
allowedOrigin = envOrigin
fmt.Printf("Allowed origin: %s\n", allowedOrigin)
logger.Info("allowed origin set", "allowedOrigin", allowedOrigin)
}

mux := http.NewServeMux()

fileServer := http.FileServer(http.Dir("./static"))
mux.Handle("/static/*", http.StripPrefix("/static/", fileServer))
mux.Handle("/favicon.ico", fileServer)

mux.HandleFunc("/",
middleware.CORS(
http.HandlerFunc(
handlers.IndexHandler,
),
allowedOrigin),
middleware.CSPMiddleware(
middleware.CORS(
http.HandlerFunc(
handlers.IndexHandler,
),
allowedOrigin),
htmxHash,
hyperscriptHash,
echartsHash,
),
)

mux.HandleFunc("/calc",
middleware.CORS(
http.HandlerFunc(
handlers.CalcHandler,
),
allowedOrigin),
middleware.CSPMiddleware(
middleware.CORS(
http.HandlerFunc(
handlers.CalcHandler,
),
allowedOrigin),
htmxHash,
hyperscriptHash,
echartsHash,
),
)

mux.HandleFunc("/healthz", http.HandlerFunc(handlers.HealthzHandler))
Expand All @@ -70,18 +89,18 @@ func main() {
log.Fatal(err)
}
}()

fmt.Printf("Server available at %s\n", serveAt)
logger.Info("server listening", "serverHost", serverHost, "serverPort", serverPort)

// Wait for interrupt signal.
<-ctx.Done()

// Sleep to ensure graceful shutdown
fmt.Println("Server shutting down...")
time.Sleep(5 * time.Second)
sleepSeconds := 5
logger.Info("shutting down", "sleepSeconds", sleepSeconds)
time.Sleep(time.Duration(sleepSeconds) * time.Second)

// Return to default context.
cancel()

fmt.Println("Server stopped")
logger.Info("server shut down gracefully")
}
129 changes: 79 additions & 50 deletions internal/calc/calc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package calc

import (
"errors"
"math"
"strings"
)
Expand Down Expand Up @@ -33,47 +34,85 @@ type DebtRecyclingData struct {
TotalValue float64
}

func taxRate(salary float64, country string) float64 {
taxBrackets := []struct {
lowerBound float64
upperBound float64
marginalRate float64
}{}
switch strings.ToLower(country) {
case "au":
taxBrackets = []struct {
lowerBound float64
upperBound float64
marginalRate float64
}{
{0, 18200, 0},
{18201, 45000, 0.16},
{45001, 135000, 0.30},
{135001, 190000, 0.37},
{190001, math.MaxFloat64, 0.45},
}
case "nz":
taxBrackets = []struct {
lowerBound float64
upperBound float64
marginalRate float64
}{
{0, 14000, 0.105},
{14001, 48000, 0.175},
{48001, 70000, 0.30},
{70001, 180000, 0.33},
{180001, math.MaxFloat64, 0.39},
}
type TaxBracket struct {
LowerBound float64
UpperBound float64
MarginalRate float64
}

type CountryBrackets []TaxBracket

var countryBrackets = map[string]CountryBrackets{
"au": {
TaxBracket{
LowerBound: 0,
UpperBound: 18200,
MarginalRate: 0,
},
TaxBracket{
LowerBound: 18201,
UpperBound: 45000,
MarginalRate: 0.16,
},
TaxBracket{
LowerBound: 45001,
UpperBound: 135000,
MarginalRate: 0.30,
},
TaxBracket{
LowerBound: 135001,
UpperBound: 190000,
MarginalRate: 0.37,
},
TaxBracket{
LowerBound: 190001,
UpperBound: math.MaxFloat64,
MarginalRate: .45,
},
},
"nz": {
TaxBracket{
LowerBound: 0,
UpperBound: 14000,
MarginalRate: 0.105,
},
TaxBracket{
LowerBound: 14001,
UpperBound: 48000,
MarginalRate: 0.175,
},
TaxBracket{
LowerBound: 48001,
UpperBound: 70000,
MarginalRate: 0.30,
},
TaxBracket{
LowerBound: 70001,
UpperBound: 180000,
MarginalRate: 0.33,
},
TaxBracket{
LowerBound: 180001,
UpperBound: math.MaxFloat64,
MarginalRate: 0.39,
},
},
}

func taxRate(salary float64, country string) (float64, error) {
taxBrackets := countryBrackets[strings.ToLower(country)]
if taxBrackets == nil {
return 0, errors.New("unknown country")
}

for i := len(taxBrackets) - 1; i >= 0; i-- {
bracket := taxBrackets[i]
if salary > bracket.lowerBound {
return bracket.marginalRate
if salary > bracket.LowerBound {
return bracket.MarginalRate, nil
}
}

return 0
return 0, nil
}

func CAGR(initialValue, finalValue float64, numYears int) float64 {
Expand All @@ -83,7 +122,7 @@ func CAGR(initialValue, finalValue float64, numYears int) float64 {
return math.Pow(finalValue/initialValue, 1/float64(numYears)) - 1
}

func DebtRecycling(params DebtRecyclingParameters) *DebtRecyclingData {
func DebtRecycling(params DebtRecyclingParameters) (*DebtRecyclingData, error) {
data := &DebtRecyclingData{}

// Pre-allocate slices with the correct size
Expand Down Expand Up @@ -129,7 +168,10 @@ func DebtRecycling(params DebtRecyclingParameters) *DebtRecyclingData {
)

// Calculate tax savings (adjusting for tax liability)
taxRate := taxRate((params.Salary + data.DividendReturns[year]), params.Country)
taxRate, err := taxRate((params.Salary + data.DividendReturns[year]), params.Country)
if err != nil {
return nil, err
}
data.TaxRefunds[year] = data.TaxDeductibleInterest[year] * (1 - taxRate)

// Accumulate tax savings
Expand Down Expand Up @@ -175,18 +217,5 @@ func DebtRecycling(params DebtRecyclingParameters) *DebtRecyclingData {
data.TotalValue = data.PortfolioValue[params.NumYears] + data.CumulativeTaxRefunds[params.NumYears-1] + data.CumulativeDividends[params.NumYears-1]
data.TotalInvested = params.InitialInvestment + (params.AnnualInvestment * float64(params.NumYears))

return data
}

func GeometricMean(rates []float64) float64 {
if len(rates) == 0 {
return 0
}

product := 1.0
for _, rate := range rates {
product *= 1 + rate
}

return math.Pow(product, 1/float64(len(rates))) - 1
return data, nil
}
Loading

0 comments on commit 143db20

Please sign in to comment.