-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #528 from Pinelab-studio/fix/metrics-sales
feat(metrics): Updates, fixes and Revenue metric
- Loading branch information
Showing
14 changed files
with
39,066 additions
and
1,940 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,32 @@ | ||
interface EntryDate { | ||
createdAt: Date; | ||
} | ||
|
||
/** | ||
* Very basic in memory cache for storing metric results | ||
*/ | ||
export class Cache<T> { | ||
private cache = new Map<string, T>(); | ||
private key(key: any): string { | ||
return JSON.stringify(key); | ||
} | ||
// Default max age is 12 hours | ||
constructor(private maxAgeInSeconds: number = 60 * 60 * 12) {} | ||
|
||
private cache = new Map<string, T & EntryDate>(); | ||
|
||
set(key: any, value: T): void { | ||
this.cache.set(this.key(key), value); | ||
set(key: string, value: T): void { | ||
this.cache.set(key, { | ||
...value, | ||
createdAt: new Date(), | ||
}); | ||
} | ||
get(key: any): T | undefined { | ||
return this.cache.get(this.key(key)); | ||
get(key: string): T | undefined { | ||
const res = this.cache.get(key); | ||
if (!res) { | ||
return undefined; | ||
} | ||
const now = new Date(); | ||
if (now.getTime() - res.createdAt.getTime() > this.maxAgeInSeconds * 1000) { | ||
this.cache.delete(key); | ||
return undefined; | ||
} | ||
return res; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
packages/vendure-plugin-metrics/src/api/metrics/revenue-per-product.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { | ||
Injector, | ||
Logger, | ||
OrderLine, | ||
ProductVariant, | ||
RequestContext, | ||
TransactionalConnection, | ||
} from '@vendure/core'; | ||
import { AdvancedMetricType } from '../../ui/generated/graphql'; | ||
import { MetricStrategy, NamedDatapoint } from '../metric-strategy'; | ||
import { loggerCtx } from '../../constants'; | ||
|
||
/** | ||
* Calculates the revenue generated by each product variant. | ||
* calculates the total revenue if no variants are provided. | ||
*/ | ||
export class RevenuePerProduct implements MetricStrategy<OrderLine> { | ||
readonly metricType: AdvancedMetricType = AdvancedMetricType.Currency; | ||
readonly code = 'revenue-per-product'; | ||
|
||
getTitle(ctx: RequestContext): string { | ||
return `Revenue`; | ||
} | ||
|
||
getSortableField(entity: OrderLine): Date { | ||
return entity.order.orderPlacedAt ?? entity.order.updatedAt; | ||
} | ||
|
||
async loadEntities( | ||
ctx: RequestContext, | ||
injector: Injector, | ||
from: Date, | ||
to: Date, | ||
variants: ProductVariant[] | ||
): Promise<OrderLine[]> { | ||
let skip = 0; | ||
const take = 1000; | ||
let hasMoreOrderLines = true; | ||
const lines: OrderLine[] = []; | ||
while (hasMoreOrderLines) { | ||
let query = injector | ||
.get(TransactionalConnection) | ||
.getRepository(ctx, OrderLine) | ||
.createQueryBuilder('orderLine') | ||
.leftJoin('orderLine.productVariant', 'productVariant') | ||
.addSelect(['productVariant.sku', 'productVariant.id']) | ||
.leftJoinAndSelect('orderLine.order', 'order') | ||
.leftJoin('order.channels', 'channel') | ||
.where(`channel.id=:channelId`, { channelId: ctx.channelId }) | ||
.andWhere(`order.orderPlacedAt >= :from`, { | ||
from: from.toISOString(), | ||
}) | ||
.andWhere(`order.orderPlacedAt <= :to`, { | ||
to: to.toISOString(), | ||
}) | ||
.offset(skip) | ||
.limit(take); | ||
if (variants.length) { | ||
query = query.andWhere(`productVariant.id IN(:...variantIds)`, { | ||
variantIds: variants.map((v) => v.id), | ||
}); | ||
} | ||
const [items, totalItems] = await query.getManyAndCount(); | ||
lines.push(...items); | ||
Logger.info( | ||
`Fetched order lines ${skip}-${skip + take} for metric ${ | ||
this.code | ||
} for channel${ctx.channel.token}`, | ||
loggerCtx | ||
); | ||
skip += items.length; | ||
if (lines.length >= totalItems) { | ||
hasMoreOrderLines = false; | ||
} | ||
} | ||
return lines; | ||
} | ||
|
||
calculateDataPoints( | ||
ctx: RequestContext, | ||
lines: OrderLine[], | ||
variants: ProductVariant[] | ||
): NamedDatapoint[] { | ||
if (!variants.length) { | ||
// Return total revenue | ||
let legendLabel = 'Total Revenue'; | ||
if (ctx.channel.pricesIncludeTax) { | ||
legendLabel += ' (incl. tax)'; | ||
} else { | ||
legendLabel += ' (excl. tax)'; | ||
} | ||
const revenuePerOrder: { [orderId: string]: number } = {}; | ||
const totalFieldName = ctx.channel.pricesIncludeTax | ||
? 'totalWithTax' | ||
: 'total'; | ||
lines.forEach((line) => { | ||
revenuePerOrder[line.order.id] = line.order[totalFieldName]; | ||
}); | ||
const totalRevenue = Object.values(revenuePerOrder).reduce( | ||
(total, current) => total + current, | ||
0 | ||
); | ||
return [ | ||
{ | ||
legendLabel, | ||
value: totalRevenue / 100, | ||
}, | ||
]; | ||
} | ||
// Else calculate revenue per variant | ||
const dataPoints: NamedDatapoint[] = []; | ||
const totalFieldName = ctx.channel.pricesIncludeTax | ||
? 'proratedLinePriceWithTax' | ||
: 'proratedLinePrice'; | ||
variants.forEach((variant) => { | ||
// Find order lines per variant id | ||
const linesForVariant = lines.filter( | ||
(line) => line.productVariant.id === variant.id | ||
); | ||
// Sum of order lines with this variant | ||
|
||
const revenue = linesForVariant.reduce( | ||
(total, current) => total + current[totalFieldName], | ||
0 | ||
); | ||
dataPoints.push({ | ||
legendLabel: variant.name, | ||
value: revenue / 100, | ||
}); | ||
}); | ||
return dataPoints; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
packages/vendure-plugin-metrics/src/ui/chartist/chartist.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.