Skip to content

Commit ec58a0a

Browse files
authored
Merge pull request afadil#577 from afadil/fix/v3-dependecies-upgrade
Fix/v3 dependecies upgrade
2 parents cc8230f + c4f97c5 commit ec58a0a

50 files changed

Lines changed: 1627 additions & 765 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

addons/swingfolio-addon/src/lib/trade-matcher-test.ts

Lines changed: 26 additions & 28 deletions
Large diffs are not rendered by default.

addons/swingfolio-addon/src/lib/trade-matcher.ts

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@ import type { ActivityDetails } from "@wealthfolio/addon-sdk";
22
import { differenceInDays } from "date-fns";
33
import type { ClosedTrade, OpenPosition, TradeMatchResult } from "../types";
44

5+
/** ActivityDetails with numeric fields parsed from string | null */
6+
type ParsedActivity = Omit<ActivityDetails, "quantity" | "unitPrice" | "fee" | "amount"> & {
7+
quantity: number;
8+
unitPrice: number;
9+
fee: number;
10+
amount: number;
11+
};
12+
513
interface Lot {
6-
activity: ActivityDetails;
14+
activity: ParsedActivity;
715
remainingQuantity: number;
816
originalQuantity: number;
9-
dividends: ActivityDetails[];
17+
dividends: ParsedActivity[];
1018
}
1119

1220
interface AverageLot {
1321
symbol: string;
1422
totalQuantity: number;
1523
totalCostBasis: number;
1624
averagePrice: number;
17-
activities: ActivityDetails[];
25+
activities: ParsedActivity[];
1826
remainingQuantity: number;
19-
dividends: ActivityDetails[];
27+
dividends: ParsedActivity[];
2028
}
2129

2230
export interface TradeMatcherOptions {
@@ -83,20 +91,23 @@ export class TradeMatcher {
8391
/**
8492
* Parse activities to ensure numeric fields are numbers
8593
*/
86-
private parseActivities(activities: ActivityDetails[]): ActivityDetails[] {
87-
return activities.map((a) => ({
88-
...a,
89-
quantity: this.parseNumber(a.quantity),
90-
unitPrice: this.parseNumber(a.unitPrice),
91-
fee: this.parseNumber(a.fee),
92-
amount: this.parseNumber(a.amount),
93-
}));
94+
private parseActivities(activities: ActivityDetails[]): ParsedActivity[] {
95+
return activities.map(
96+
(a) =>
97+
({
98+
...a,
99+
quantity: this.parseNumber(a.quantity),
100+
unitPrice: this.parseNumber(a.unitPrice),
101+
fee: this.parseNumber(a.fee),
102+
amount: this.parseNumber(a.amount),
103+
}) as ParsedActivity,
104+
);
94105
}
95106

96107
/**
97-
* Safely parse a value to number
108+
* Safely parse a string | number | null value to number.
98109
*/
99-
private parseNumber(value: any): number {
110+
private parseNumber(value: string | number | null | undefined): number {
100111
if (typeof value === "number") return value;
101112
if (typeof value === "string") return parseFloat(value) || 0;
102113
return 0;
@@ -105,7 +116,7 @@ export class TradeMatcher {
105116
/**
106117
* Group activities by symbol
107118
*/
108-
private groupBySymbol(activities: ActivityDetails[]): Record<string, ActivityDetails[]> {
119+
private groupBySymbol(activities: ParsedActivity[]): Record<string, ParsedActivity[]> {
109120
return activities.reduce(
110121
(acc, activity) => {
111122
const symbol = activity.assetSymbol;
@@ -115,7 +126,7 @@ export class TradeMatcher {
115126
acc[symbol].push(activity);
116127
return acc;
117128
},
118-
{} as Record<string, ActivityDetails[]>,
129+
{} as Record<string, ParsedActivity[]>,
119130
);
120131
}
121132

@@ -124,8 +135,8 @@ export class TradeMatcher {
124135
*/
125136
private matchSymbolTrades(
126137
symbol: string,
127-
activities: ActivityDetails[],
128-
dividends: ActivityDetails[] = [],
138+
activities: ParsedActivity[],
139+
dividends: ParsedActivity[] = [],
129140
): TradeMatchResult {
130141
// Sort activities chronologically
131142
const sortedActivities = [...activities].sort(
@@ -144,8 +155,8 @@ export class TradeMatcher {
144155
*/
145156
private matchSymbolTradesAverage(
146157
symbol: string,
147-
activities: ActivityDetails[],
148-
dividends: ActivityDetails[] = [],
158+
activities: ParsedActivity[],
159+
dividends: ParsedActivity[] = [],
149160
): TradeMatchResult {
150161
const closedTrades: ClosedTrade[] = [];
151162
const openPositions: OpenPosition[] = [];
@@ -181,7 +192,7 @@ export class TradeMatcher {
181192
} else if (activity.activityType === "SELL") {
182193
// Process sell against average lot
183194
if (!averageLot || averageLot.remainingQuantity <= 0) {
184-
unmatchedSells.push(activity);
195+
unmatchedSells.push(activity as unknown as ActivityDetails);
185196
continue;
186197
}
187198

@@ -214,7 +225,7 @@ export class TradeMatcher {
214225
unmatchedSells.push({
215226
...activity,
216227
quantity: sellQuantityRemaining,
217-
});
228+
} as unknown as ActivityDetails);
218229
}
219230
}
220231
}
@@ -238,7 +249,7 @@ export class TradeMatcher {
238249
/**
239250
* Create a new average lot
240251
*/
241-
private createNewAverageLot(activity: ActivityDetails, symbol: string): AverageLot {
252+
private createNewAverageLot(activity: ParsedActivity, symbol: string): AverageLot {
242253
return {
243254
symbol,
244255
totalQuantity: activity.quantity,
@@ -253,7 +264,7 @@ export class TradeMatcher {
253264
/**
254265
* Update existing average lot with new buy activity
255266
*/
256-
private updateAverageLot(averageLot: AverageLot, activity: ActivityDetails): void {
267+
private updateAverageLot(averageLot: AverageLot, activity: ParsedActivity): void {
257268
const newTotalQuantity = averageLot.remainingQuantity + activity.quantity;
258269
const newTotalCostBasis =
259270
averageLot.averagePrice * averageLot.remainingQuantity +
@@ -271,8 +282,8 @@ export class TradeMatcher {
271282
*/
272283
private matchSymbolTradesSpecific(
273284
symbol: string,
274-
activities: ActivityDetails[],
275-
dividends: ActivityDetails[] = [],
285+
activities: ParsedActivity[],
286+
dividends: ParsedActivity[] = [],
276287
): TradeMatchResult {
277288
const closedTrades: ClosedTrade[] = [];
278289
const openPositions: OpenPosition[] = [];
@@ -324,7 +335,7 @@ export class TradeMatcher {
324335
unmatchedSells.push({
325336
...activity,
326337
quantity: sellQuantityRemaining,
327-
});
338+
} as unknown as ActivityDetails);
328339
}
329340
}
330341
}
@@ -352,7 +363,7 @@ export class TradeMatcher {
352363
*/
353364
private createClosedTradeAverage(
354365
averageLot: AverageLot,
355-
sellActivity: ActivityDetails,
366+
sellActivity: ParsedActivity,
356367
quantity: number,
357368
symbol: string,
358369
): ClosedTrade {
@@ -456,11 +467,11 @@ export class TradeMatcher {
456467
* Create a closed trade from specific lot matching
457468
*/
458469
private createClosedTrade(
459-
buyActivity: ActivityDetails,
460-
sellActivity: ActivityDetails,
470+
buyActivity: ParsedActivity,
471+
sellActivity: ParsedActivity,
461472
quantity: number,
462473
symbol: string,
463-
dividends: ActivityDetails[] = [],
474+
dividends: ParsedActivity[] = [],
464475
): ClosedTrade {
465476
const entryDate = new Date(buyActivity.date);
466477
const exitDate = new Date(sellActivity.date);
@@ -551,7 +562,7 @@ export class TradeMatcher {
551562
private calculateTradeDividends(
552563
entryDate: Date,
553564
exitDate: Date,
554-
dividends: ActivityDetails[],
565+
dividends: ParsedActivity[],
555566
): number {
556567
if (!this.includeDividends || dividends.length === 0) return 0;
557568

addons/swingfolio-addon/src/pages/activity-selector-page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,11 @@ export default function ActivitySelectorPage({ ctx }: ActivitySelectorPageProps)
306306
</div>
307307
)}
308308
</td>
309-
<td className="p-3 text-sm">{activity.quantity.toLocaleString()}</td>
310309
<td className="p-3 text-sm">
311-
{activity.unitPrice.toLocaleString("en-US", {
310+
{Number(activity.quantity ?? 0).toLocaleString()}
311+
</td>
312+
<td className="p-3 text-sm">
313+
{Number(activity.unitPrice ?? 0).toLocaleString("en-US", {
312314
style: "currency",
313315
currency: activity.currency,
314316
})}

addons/swingfolio-addon/test-runner.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function parseCSV(csv) {
1717
obj[header] = value ? parseFloat(value) : 0;
1818
} else if (["date", "createdAt", "updatedAt"].includes(header)) {
1919
obj[header] = new Date(value);
20-
} else if (["isDraft"].includes(header)) {
20+
} else if (["needsReview"].includes(header)) {
2121
obj[header] = value === "TRUE";
2222
} else {
2323
obj[header] = value;

apps/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"lodash": "^4.17.23",
5353
"lucide-react": "^0.561.0",
5454
"motion": "^12.34.0",
55+
"nanoid": "^5.1.6",
5556
"qrcode.react": "^4.2.0",
5657
"react": "^19.2.4",
5758
"react-day-picker": "^9.13.2",

apps/frontend/src/components/ticker-search.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useQuery } from "@tanstack/react-query";
1717
import { useComposedRefs } from "@wealthfolio/ui/hooks";
1818
import { Command as CommandPrimitive } from "cmdk";
1919
import { debounce } from "lodash";
20-
import { forwardRef, memo, useCallback, useMemo, useRef, useState } from "react";
20+
import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
2121
import { CreateCustomAssetDialog } from "./create-custom-asset-dialog";
2222

2323
interface SearchProps {
@@ -35,6 +35,8 @@ interface SearchProps {
3535
className?: string;
3636
/** Default currency to use for custom assets (typically from account) */
3737
defaultCurrency?: string;
38+
/** Optional exchange MIC for display context when value is canonical (e.g., SHOP -> SHOP (TSX)). */
39+
selectedExchangeMic?: string;
3840
/** Test ID for e2e testing */
3941
"data-testid"?: string;
4042
}
@@ -190,6 +192,7 @@ const TickerSearchInput = forwardRef<HTMLButtonElement, SearchProps>(
190192
autoFocusSearch = false,
191193
className,
192194
defaultCurrency,
195+
selectedExchangeMic,
193196
"data-testid": testId,
194197
},
195198
ref,
@@ -212,6 +215,10 @@ const TickerSearchInput = forwardRef<HTMLButtonElement, SearchProps>(
212215
return defaultValue;
213216
}
214217
if (value) {
218+
const exchangeDisplay = getExchangeDisplayName(selectedExchangeMic);
219+
if (exchangeDisplay) {
220+
return `${value} (${exchangeDisplay})`;
221+
}
215222
return value;
216223
}
217224
return "";
@@ -258,6 +265,18 @@ const TickerSearchInput = forwardRef<HTMLButtonElement, SearchProps>(
258265
[onSelectResult, debouncedSearch, isControlled, onOpenChange],
259266
);
260267

268+
useEffect(() => {
269+
if (selectedResult) return;
270+
const current = value ?? defaultValue ?? "";
271+
if (!current) {
272+
setSelected("");
273+
return;
274+
}
275+
const exchangeDisplay = getExchangeDisplayName(selectedExchangeMic);
276+
const next = exchangeDisplay ? `${current} (${exchangeDisplay})` : current;
277+
setSelected((prev) => (prev === next ? prev : next));
278+
}, [defaultValue, selectedExchangeMic, selectedResult, value]);
279+
261280
// Handle "Create custom asset" click
262281
const handleCreateCustomAsset = useCallback(() => {
263282
if (isControlled) {

apps/frontend/src/features/ai-assistant/hooks/use-chat-runtime.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useQueryClient, type InfiniteData } from "@tanstack/react-query";
2020
import { streamChatResponse, type ChatModelConfig } from "../api";
2121
import type { AiThread, ChatMessage, ChatThread, ThreadPage } from "../types";
2222
import { QueryKeys } from "@/lib/query-keys";
23+
import { generateId } from "@/lib/id";
2324
import { AI_THREADS_KEY } from "./use-threads";
2425
import { deleteAiThread, getAiThreadMessages, updateAiThread } from "@/adapters";
2526

@@ -212,7 +213,7 @@ const csvAttachmentAdapter: AttachmentAdapter = {
212213

213214
async add({ file }): Promise<PendingAttachment> {
214215
return {
215-
id: crypto.randomUUID(),
216+
id: generateId(),
216217
type: "document",
217218
name: file.name,
218219
contentType: file.type || "text/csv",
@@ -484,7 +485,7 @@ export function useChatRuntime(config?: ChatModelConfig) {
484485

485486
// Create user message for UI display
486487
const userMessage: ExternalMessage = {
487-
id: crypto.randomUUID(),
488+
id: generateId(),
488489
role: "user",
489490
parts:
490491
userMessageParts.length > 0 ? userMessageParts : [{ type: "text", content: "(empty)" }],
@@ -495,7 +496,7 @@ export function useChatRuntime(config?: ChatModelConfig) {
495496
setMessages((prev) => [...prev, userMessage]);
496497

497498
// Create placeholder assistant message
498-
const assistantMessageId = crypto.randomUUID();
499+
const assistantMessageId = generateId();
499500
const assistantMessage: ExternalMessage = {
500501
id: assistantMessageId,
501502
role: "assistant",

apps/frontend/src/lib/id.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { nanoid as secureNanoid } from "nanoid";
2+
import { nanoid as nonSecureNanoid } from "nanoid/non-secure";
3+
4+
function hasCryptoSupport(): boolean {
5+
if (typeof globalThis === "undefined") return false;
6+
const cryptoObj = (globalThis as { crypto?: Crypto }).crypto;
7+
return typeof cryptoObj?.getRandomValues === "function";
8+
}
9+
10+
export function generateId(prefix?: string): string {
11+
const id = hasCryptoSupport() ? secureNanoid() : nonSecureNanoid();
12+
return prefix ? `${prefix}-${id}` : id;
13+
}

apps/frontend/src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ export interface UpdateAssetProfile {
807807
notes?: string | null;
808808
kind?: AssetKind | null;
809809
quoteMode?: QuoteMode | null;
810+
instrumentExchangeMic?: string | null;
810811
providerConfig?: Record<string, unknown> | null;
811812
}
812813

apps/frontend/src/pages/activity/components/activity-data-grid/use-activity-columns.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ActivityStatus,
55
ActivityType,
66
ActivityTypeNames,
7+
getExchangeDisplayName,
78
SUBTYPE_DISPLAY_NAMES,
89
SUBTYPES_BY_ACTIVITY_TYPE,
910
} from "@/lib/constants";
@@ -225,6 +226,14 @@ export function useActivityColumns({
225226
const row = rowData as LocalTransaction;
226227
return isCashActivity(row.activityType ?? "");
227228
},
229+
getDisplayContext: (rowData: unknown) => {
230+
const row = rowData as LocalTransaction;
231+
const symbol = (row.assetSymbol ?? "").trim().toUpperCase();
232+
if (!symbol || symbol === "CASH" || symbol.startsWith("$CASH")) {
233+
return undefined;
234+
}
235+
return getExchangeDisplayName(row.exchangeMic);
236+
},
228237
isClearable: (rowData: unknown) => {
229238
const row = rowData as LocalTransaction;
230239
return !isSymbolRequired(row.activityType ?? "");

0 commit comments

Comments
 (0)