Skip to content

Commit

Permalink
Simple transaction overview (#246)
Browse files Browse the repository at this point in the history
* Simple transaction overview

* Added test for GetPortfolioTransaction

* Added simple UI form for viewing and editing transactions
  • Loading branch information
oxisto authored Dec 15, 2023
1 parent 842adb5 commit 4a22534
Show file tree
Hide file tree
Showing 21 changed files with 1,218 additions and 578 deletions.
1,024 changes: 548 additions & 476 deletions gen/mgo.pb.go

Large diffs are not rendered by default.

141 changes: 116 additions & 25 deletions gen/portfoliov1connect/mgo.connect.go

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion mgo.proto
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ message GetPortfolioSnapshotRequest {

message CreatePortfolioTransactionRequest { PortfolioEvent transaction = 1; }

message GetPortfolioTransactionRequest { string name = 1; }

message ListPortfolioTransactionsRequest { string portfolio_name = 1; }

message ListPortfolioTransactionsResponse {
Expand Down Expand Up @@ -161,8 +163,14 @@ service PortfolioService {

rpc CreatePortfolioTransaction(CreatePortfolioTransactionRequest)
returns (PortfolioEvent);
rpc GetPortfolioTransaction(GetPortfolioTransactionRequest)
returns (PortfolioEvent) {
option idempotency_level = NO_SIDE_EFFECTS;
};
rpc ListPortfolioTransactions(ListPortfolioTransactionsRequest)
returns (ListPortfolioTransactionsResponse);
returns (ListPortfolioTransactionsResponse) {
option idempotency_level = NO_SIDE_EFFECTS;
};
rpc UpdatePortfolioTransaction(UpdatePortfolioTransactionRequest)
returns (PortfolioEvent);
rpc DeletePortfolioTransaction(DeletePortfolioTransactionRequest)
Expand Down
23 changes: 21 additions & 2 deletions service/portfolio/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ func (svc *service) CreatePortfolioTransaction(ctx context.Context, req *connect
// Create a unique name for the transaction
req.Msg.Transaction.MakeUniqueName()

slog.Info("Creating transaction", "transaction", req.Msg.Transaction)
slog.Info(
"Creating transaction",
"transaction", req.Msg.Transaction,
)

return crud.Create(
req.Msg.Transaction,
Expand All @@ -46,6 +49,16 @@ func (svc *service) CreatePortfolioTransaction(ctx context.Context, req *connect
)
}

func (svc *service) GetPortfolioTransaction(ctx context.Context, req *connect.Request[portfoliov1.GetPortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) {
return crud.Get(
req.Msg.Name,
svc.events,
func(obj *portfoliov1.PortfolioEvent) *portfoliov1.PortfolioEvent {
return obj
},
)
}

func (svc *service) ListPortfolioTransactions(ctx context.Context, req *connect.Request[portfoliov1.ListPortfolioTransactionsRequest]) (res *connect.Response[portfoliov1.ListPortfolioTransactionsResponse], err error) {
return crud.List(
svc.events,
Expand All @@ -59,7 +72,13 @@ func (svc *service) ListPortfolioTransactions(ctx context.Context, req *connect.
)
}

func (svc *service) UpdatePortfolioTransactions(ctx context.Context, req *connect.Request[portfoliov1.UpdatePortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) {
func (svc *service) UpdatePortfolioTransaction(ctx context.Context, req *connect.Request[portfoliov1.UpdatePortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) {
slog.Info(
"Updating transaction",
"tx", req.Msg.Transaction,
"update-mask", req.Msg.UpdateMask.Paths,
)

return crud.Update(
req.Msg.Transaction.Name,
req.Msg.Transaction,
Expand Down
52 changes: 50 additions & 2 deletions service/portfolio/transactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,54 @@ func Test_service_CreatePortfolioTransaction(t *testing.T) {
}
}

func Test_service_GetPortfolioTransaction(t *testing.T) {
type fields struct {
portfolios persistence.StorageOperations[*portfoliov1.Portfolio]
securities portfoliov1connect.SecuritiesServiceClient
}
type args struct {
ctx context.Context
req *connect.Request[portfoliov1.GetPortfolioTransactionRequest]
}
tests := []struct {
name string
fields fields
args args
wantRes assert.Want[*connect.Response[portfoliov1.PortfolioEvent]]
wantErr bool
}{
{
name: "happy path",
fields: fields{
portfolios: myPortfolio(t),
},
args: args{
req: connect.NewRequest(&portfoliov1.GetPortfolioTransactionRequest{
Name: "buy",
}),
},
wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioEvent]) bool {
return assert.Equals(t, "buy", r.Msg.Name) && assert.Equals(t, "bank/myportfolio", r.Msg.PortfolioName)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := &service{
portfolios: tt.fields.portfolios,
events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios),
securities: tt.fields.securities,
}
gotRes, err := svc.GetPortfolioTransaction(tt.args.ctx, tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("service.GetPortfolioTransaction() error = %v, wantErr %v", err, tt.wantErr)
return
}
tt.wantRes(t, gotRes)
})
}
}

func Test_service_ListPortfolioTransactions(t *testing.T) {
type fields struct {
portfolios persistence.StorageOperations[*portfoliov1.Portfolio]
Expand Down Expand Up @@ -134,7 +182,7 @@ func Test_service_ListPortfolioTransactions(t *testing.T) {
}
}

func Test_service_UpdatePortfolioTransactions(t *testing.T) {
func Test_service_UpdatePortfolioTransaction(t *testing.T) {
type fields struct {
portfolios persistence.StorageOperations[*portfoliov1.Portfolio]
securities portfoliov1connect.SecuritiesServiceClient
Expand Down Expand Up @@ -177,7 +225,7 @@ func Test_service_UpdatePortfolioTransactions(t *testing.T) {
events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios),
securities: tt.fields.securities,
}
gotRes, err := svc.UpdatePortfolioTransactions(tt.args.ctx, tt.args.req)
gotRes, err := svc.UpdatePortfolioTransaction(tt.args.ctx, tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("service.UpdatePortfolioTransactions() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down
7 changes: 7 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"autoprefixer": "^10.4.15",
"dayjs": "^1.11.10",
"eslint": "^8.28.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.30.0",
Expand Down
45 changes: 45 additions & 0 deletions ui/src/lib/components/DateTimeInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { Timestamp } from '@bufbuild/protobuf';
import dayjs from 'dayjs';
export let format = 'YYYY-MM-DD HH:mm';
export let date: Timestamp | undefined = new Timestamp();
export let initial = false;
let internal: string;
function input(x: Timestamp | undefined) {
console.log(initial);
if (initial) {
return;
} else {
// only do this once because otherwise we encounter a bug where the year 0002 is parsed as 1902
initial = true;
if (x !== undefined) {
internal = dayjs(x.toDate()).format(format);
} else {
internal == '';
}
}
}
function output(x: string) {
if (x !== '') {
date = Timestamp.fromDate(dayjs(x, format).toDate());
} else {
//date = undefined;
}
}
$: input(date);
$: output(internal);
</script>

<input
type="datetime-local"
name="type"
id="type"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
bind:value={internal}
/>
105 changes: 54 additions & 51 deletions ui/src/lib/components/PortfolioPositionsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@
<TableSorter
active={sortBy == 'displayName'}
column="displayName"
on:change-direction={toggleSortDirection}
on:change-sort-by={(column) => changeSortBy('displayName')}
on:changeDirection={toggleSortDirection}
on:changeSortBy={(column) => changeSortBy('displayName')}
>
Name
</TableSorter>
Expand All @@ -77,8 +77,8 @@
<TableSorter
active={sortBy == 'amount'}
column="amount"
on:change-direction={toggleSortDirection}
on:change-sort-by={(column) => changeSortBy('amount')}>Amount</TableSorter
on:changeDirection={toggleSortDirection}
on:changeSortBy={(column) => changeSortBy('amount')}>Amount</TableSorter
>
</th>
<th
Expand All @@ -88,8 +88,8 @@
<TableSorter
active={sortBy == 'purchaseValue'}
column="purchaseValue"
on:change-direction={toggleSortDirection}
on:change-sort-by={(column) => changeSortBy('purchaseValue')}
on:changeDirection={toggleSortDirection}
on:changeSortBy={(column) => changeSortBy('purchaseValue')}
>Purchase Value
</TableSorter>
</th>
Expand All @@ -100,16 +100,16 @@
<TableSorter
active={sortBy == 'marketValue'}
column="marketValue"
on:change-direction={toggleSortDirection}
on:change-sort-by={(column) => changeSortBy('marketValue')}>Market Value</TableSorter
on:changeDirection={toggleSortDirection}
on:changeSortBy={(column) => changeSortBy('marketValue')}>Market Value</TableSorter
>
</th>
<th scope="col" class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900">
<TableSorter
active={sortBy == 'profit'}
column="profit"
on:change-direction={toggleSortDirection}
on:change-sort-by={(column) => changeSortBy('profit')}>Profit/Loss</TableSorter
on:changeDirection={toggleSortDirection}
on:changeSortBy={(column) => changeSortBy('profit')}>Profit/Loss</TableSorter
>
</th>
</tr>
Expand All @@ -120,47 +120,50 @@
{/each}
</tbody>
<tfoot>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
>Total</th
>
<th
scope="col"
class="hidden px-3 py-3.5 text-right text-sm font-semibold text-gray-900 md:table-cell"
></th>
<th
scope="col"
class="hidden px-3 py-3.5 text-right text-sm font-semibold text-gray-900 lg:table-cell"
>
{currency(snapshot.totalPurchaseValue, 'EUR')}
</th>
<th
scope="col"
class="hidden px-3 py-3.5 text-right text-sm font-semibold text-gray-900 sm:table-cell"
>
{currency(snapshot.totalMarketValue, 'EUR')}
</th>
<th
scope="col"
class="{perf < 0
? 'text-red-500'
: perf <= 1
? 'text-gray-500'
: 'text-green-500'} px-3 py-3.5 text-right text-sm font-semibold"
>
<div>
{Intl.NumberFormat(navigator.language, {
maximumFractionDigits: 2
}).format(perf)} %
<Icon
src={perf < 0 ? ArrowDown : perf < 1 ? ArrowRight : ArrowUp}
class="float-right mt-0.5 h-4 w-4"
aria-hidden="true"
/>
</div>
<div class="pr-4">
{currency(snapshot.totalMarketValue - snapshot.totalPurchaseValue, 'EUR')}
</div>
</th>
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Total</th
>
<th
scope="col"
class="hidden px-3 py-3.5 text-right text-sm font-semibold text-gray-900 md:table-cell"
></th>
<th
scope="col"
class="hidden px-3 py-3.5 text-right text-sm font-semibold text-gray-900 lg:table-cell"
>
{currency(snapshot.totalPurchaseValue, 'EUR')}
</th>
<th
scope="col"
class="hidden px-3 py-3.5 text-right text-sm font-semibold text-gray-900 sm:table-cell"
>
{currency(snapshot.totalMarketValue, 'EUR')}
</th>
<th
scope="col"
class="{perf < 0
? 'text-red-500'
: perf <= 1
? 'text-gray-500'
: 'text-green-500'} px-3 py-3.5 text-right text-sm font-semibold"
>
<div>
{Intl.NumberFormat(navigator.language, {
maximumFractionDigits: 2
}).format(perf)} %
<Icon
src={perf < 0 ? ArrowDown : perf < 1 ? ArrowRight : ArrowUp}
class="float-right mt-0.5 h-4 w-4"
aria-hidden="true"
/>
</div>
<div class="pr-4">
{currency(snapshot.totalMarketValue - snapshot.totalPurchaseValue, 'EUR')}
</div>
</th>
</tr>
</tfoot>
</table>
</div>
Loading

0 comments on commit 4a22534

Please sign in to comment.