Skip to content
This repository was archived by the owner on Apr 17, 2025. It is now read-only.

Commit 530dce9

Browse files
authored
refactor: add Money entity (#49)
* refactor: add `Money` entity * refactor: remove `formatFiat`
1 parent 7ed2e26 commit 530dce9

File tree

8 files changed

+66
-27
lines changed

8 files changed

+66
-27
lines changed

src/modules/statistics/commands/get-monthly-stats/get-monthly-stats.service.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { dinero, toDecimal } from 'dinero.js'
21
import { Usecase } from '../../../../common/domain/usecase/usecase'
3-
import { Distance } from '../../../geocoding/entities/distance'
42
import { TripRepository } from '../../../trips/trip.repository'
5-
import { PLN } from '@dinero.js/currencies'
63
import { Injectable } from '@nestjs/common'
74
import { GetMonthlyStatsResponse } from './get-monthly-stats.response'
85
import { Formatter } from '../../../../utilities/formatter'
6+
import { Money } from '../../../trips/entities/money'
97

108
@Injectable()
119
export class GetMonthlyStatsService implements Usecase<undefined, GetMonthlyStatsResponse[]> {
@@ -22,7 +20,7 @@ export class GetMonthlyStatsService implements Usecase<undefined, GetMonthlyStat
2220
day: Formatter.formatDateToMonthAndDayOfMonthFormat(summarised.day),
2321
total_distance: Formatter.formatDistance(summarised.totalDistance),
2422
avg_ride: Formatter.formatDistance(summarised.averageDistance),
25-
avg_price: Formatter.formatFiat(summarised.averagePrice),
23+
avg_price: Money.fromFloat(summarised.averagePrice).toString(),
2624
})
2725
})
2826

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { dinero, toDecimal } from 'dinero.js'
21
import { Usecase } from '../../../../common/domain/usecase/usecase'
32
import { Distance } from '../../../geocoding/entities/distance'
43
import { TripRepository } from '../../../trips/trip.repository'
54
import { GetWeeklyStatsResponse } from './get-weekly.stats.response'
6-
import { PLN } from '@dinero.js/currencies'
75
import { Injectable } from '@nestjs/common'
8-
import { Formatter } from '../../../../utilities/formatter'
6+
import { Money } from '../../../trips/entities/money'
97

108
@Injectable()
119
export class GetWeeklyStatsService implements Usecase<undefined, GetWeeklyStatsResponse> {
@@ -17,7 +15,7 @@ export class GetWeeklyStatsService implements Usecase<undefined, GetWeeklyStatsR
1715

1816
return {
1917
total_distance: new Distance(distance).toString(),
20-
total_price: Formatter.formatFiat(price),
18+
total_price: Money.fromFloat(price).toString(),
2119
}
2220
}
2321
}

src/modules/trips/commands/create-trip/create-trip.service.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ import { CreateTripResponse } from './data-transfering/create-trip.response'
44
import { TripRepository } from '../../trip.repository'
55
import { CreateTripRequest } from './data-transfering/create-trip.request'
66
import { Trip } from '../../trip.entity'
7-
import { dinero } from 'dinero.js'
8-
import { PLN } from '@dinero.js/currencies'
97
import { Usecase } from '../../../../common/domain/usecase/usecase'
10-
import { Formatter } from '../../../../utilities/formatter'
118
import { InvalidAddressError } from '../../../geocoding/errors/invalid-address.error'
129
import { Result, err, ok } from 'neverthrow'
1310
import { Coordinates } from '../../../geocoding/entities/coordinates'
11+
import { Money } from '../../entities/money'
1412

1513
@Injectable()
1614
export class CreateTripService implements Usecase<CreateTripRequest, Result<CreateTripResponse, InvalidAddressError>> {
@@ -37,7 +35,7 @@ export class CreateTripService implements Usecase<CreateTripRequest, Result<Crea
3735

3836
const distance = coordinatedOfStartingPoint.getDistanceBetweenCoordinates(coordinatedOfDestinationPoint)
3937

40-
const price = dinero({ amount: Number.parseFloat((request.price * 100).toFixed(2)), currency: PLN })
38+
const price = Money.fromFloat(request.price)
4139

4240
let trip = new Trip({
4341
startAddress: request.start_address,
@@ -55,7 +53,7 @@ export class CreateTripService implements Usecase<CreateTripRequest, Result<Crea
5553
id: trip.id,
5654
startAddress: trip.properties.startAddress,
5755
endAddress: trip.properties.endAddress,
58-
price: Formatter.formatFiat(trip.properties.price.toJSON().amount / 100),
56+
price: trip.properties.price.toString(),
5957
date: trip.properties.date,
6058
distance: trip.properties.distance.toString(),
6159
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { PLN } from '@dinero.js/currencies'
2+
import { Money } from './money'
3+
4+
describe('Money', () => {
5+
it('should return money in float', () => {
6+
const money = new Money(100, PLN)
7+
expect(money.toFloat()).toBe(1)
8+
})
9+
10+
it('should return money in string', () => {
11+
const money = Money.fromFloat(1, PLN)
12+
expect(money.toString()).toBe('1.00PLN')
13+
})
14+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { PLN } from '@dinero.js/currencies'
2+
import { Currency, Dinero, dinero, toDecimal } from 'dinero.js'
3+
import { Formatter } from '../../../utilities/formatter'
4+
5+
export class Money {
6+
private readonly baseAmount: Dinero<number>
7+
8+
constructor(baseAmount: number, public readonly currency: Currency<number>) {
9+
if (baseAmount < 0) {
10+
throw new Error('Money amount cannot be negative')
11+
}
12+
13+
if (currency !== PLN) {
14+
throw new Error('Money currency must be PLN')
15+
}
16+
17+
this.baseAmount = dinero({ amount: baseAmount, currency })
18+
}
19+
20+
static fromFloat(amount: number, currency: Currency<number> = PLN): Money {
21+
const amountInGrosze = amount * 100
22+
const amountInGroszeRounded = Math.round(amountInGrosze)
23+
return new Money(amountInGroszeRounded, currency)
24+
}
25+
26+
toString(): string {
27+
const amountInDecimal = toDecimal(this.baseAmount)
28+
let valueInSubunits = Number.parseFloat(amountInDecimal) * 100
29+
valueInSubunits = Math.round(valueInSubunits)
30+
31+
return toDecimal(
32+
dinero({ amount: valueInSubunits, currency: PLN }),
33+
({ value, currency }) => `${value}${currency.code}`,
34+
)
35+
}
36+
37+
toFloat(): number {
38+
return parseFloat(toDecimal(this.baseAmount))
39+
}
40+
}

src/modules/trips/trip.entity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Entity } from '../../common/domain/entity'
22
import { Coordinates } from '../geocoding/entities/coordinates'
33
import { Distance } from '../geocoding/entities/distance'
4-
import { Dinero } from 'dinero.js'
4+
import { Money } from './entities/money'
55

66
interface TripProperties {
77
startAddress: string
88
endAddress: string
9-
price: Dinero<number>
9+
price: Money
1010
date: Date
1111
distance?: Distance
1212
startCoordinates?: Coordinates

src/modules/trips/trip.mapper.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { Trip } from './trip.entity'
22
import { Prisma, Trip as _Trip } from '@prisma/client'
33
import { Injectable } from '@nestjs/common'
4-
import { dinero } from 'dinero.js'
5-
import { PLN } from '@dinero.js/currencies'
64
import { Distance } from '../geocoding/entities/distance'
75
import { DatabaseRecords } from '../../configuration/database-records'
86
import { Mapper } from '../../common/persistance/mapper'
7+
import { Money } from './entities/money'
98

109
@Injectable()
1110
export class TripMapper implements Mapper<Trip, DatabaseRecords.TripCreateRecord, DatabaseRecords.TripRecord> {
1211
toPersistence(entity: Trip): Prisma.TripCreateInput {
1312
return {
1413
startingAddress: entity.properties.startAddress,
1514
endingAddress: entity.properties.endAddress,
16-
priceInPLN: entity.properties.price.toJSON().amount / 100,
15+
priceInPLN: entity.properties.price.toFloat(),
1716
date: entity.properties.date,
1817
distanceInKilometers: entity.properties.distance.baseScalar,
1918
}
@@ -23,7 +22,7 @@ export class TripMapper implements Mapper<Trip, DatabaseRecords.TripCreateRecord
2322
{
2423
startAddress: record.startingAddress,
2524
endAddress: record.endingAddress,
26-
price: dinero({ amount: record.priceInPLN * 100, currency: PLN }),
25+
price: Money.fromFloat(record.priceInPLN),
2726
date: record.date,
2827
distance: new Distance(record.distanceInKilometers),
2928
},

src/utilities/formatter.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
import { PLN } from '@dinero.js/currencies'
2-
import { dinero, toDecimal } from 'dinero.js'
31
import { Distance } from '../modules/geocoding/entities/distance'
42

53
// eslint-disable-next-line @typescript-eslint/no-namespace
64
export namespace Formatter {
7-
export function formatFiat(fiat: number) {
8-
return toDecimal(
9-
dinero({ amount: Number.parseFloat(fiat.toFixed(2)) * 100, currency: PLN }),
10-
({ value, currency }) => `${value}${currency.code}`,
11-
)
12-
}
135
export function formatDistance(distance: number) {
146
return new Distance(distance).toString()
157
}

0 commit comments

Comments
 (0)