Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple transaction overview #246

Merged
merged 4 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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