forked from mirrors/homebox
d6da63187b
* add date format and orDefault helpers * introduce new statistics calculations queries * rework statistics endpoints * code generation * fix styles on photo card * label and location aggregation endpoints * code-gen * cleanup parser and defaults * remove debug point * setup E2E Testing * linters * formatting * fmt plus name support on time series data * code gen
309 lines
8.3 KiB
Go
309 lines
8.3 KiB
Go
package repo
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"entgo.io/ent/dialect/sql"
|
|
"github.com/google/uuid"
|
|
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
|
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
|
|
"github.com/hay-kot/homebox/backend/internal/data/ent/groupinvitationtoken"
|
|
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
|
|
"github.com/hay-kot/homebox/backend/internal/data/ent/label"
|
|
"github.com/hay-kot/homebox/backend/internal/data/ent/location"
|
|
)
|
|
|
|
type GroupRepository struct {
|
|
db *ent.Client
|
|
}
|
|
|
|
type (
|
|
Group struct {
|
|
ID uuid.UUID `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
|
Currency string `json:"currency,omitempty"`
|
|
}
|
|
|
|
GroupUpdate struct {
|
|
Name string `json:"name"`
|
|
Currency string `json:"currency"`
|
|
}
|
|
|
|
GroupInvitationCreate struct {
|
|
Token []byte `json:"-"`
|
|
ExpiresAt time.Time `json:"expiresAt"`
|
|
Uses int `json:"uses"`
|
|
}
|
|
|
|
GroupInvitation struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ExpiresAt time.Time `json:"expiresAt"`
|
|
Uses int `json:"uses"`
|
|
Group Group `json:"group"`
|
|
}
|
|
|
|
GroupStatistics struct {
|
|
TotalUsers int `json:"totalUsers"`
|
|
TotalItems int `json:"totalItems"`
|
|
TotalLocations int `json:"totalLocations"`
|
|
TotalLabels int `json:"totalLabels"`
|
|
TotalItemPrice float64 `json:"totalItemPrice"`
|
|
TotalWithWarranty int `json:"totalWithWarranty"`
|
|
}
|
|
|
|
ValueOverTimeEntry struct {
|
|
Date time.Time `json:"date"`
|
|
Value float64 `json:"value"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
ValueOverTime struct {
|
|
PriceAtStart float64 `json:"valueAtStart"`
|
|
PriceAtEnd float64 `json:"valueAtEnd"`
|
|
Start time.Time `json:"start"`
|
|
End time.Time `json:"end"`
|
|
Entries []ValueOverTimeEntry `json:"entries"`
|
|
}
|
|
|
|
TotalsByOrganizer struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Total float64 `json:"total"`
|
|
}
|
|
)
|
|
|
|
var (
|
|
mapToGroupErr = mapTErrFunc(mapToGroup)
|
|
)
|
|
|
|
func mapToGroup(g *ent.Group) Group {
|
|
return Group{
|
|
ID: g.ID,
|
|
Name: g.Name,
|
|
CreatedAt: g.CreatedAt,
|
|
UpdatedAt: g.UpdatedAt,
|
|
Currency: strings.ToUpper(g.Currency.String()),
|
|
}
|
|
}
|
|
|
|
var (
|
|
mapToGroupInvitationErr = mapTErrFunc(mapToGroupInvitation)
|
|
)
|
|
|
|
func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation {
|
|
return GroupInvitation{
|
|
ID: g.ID,
|
|
ExpiresAt: g.ExpiresAt,
|
|
Uses: g.Uses,
|
|
Group: mapToGroup(g.Edges.Group),
|
|
}
|
|
}
|
|
|
|
func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) {
|
|
var v []TotalsByOrganizer
|
|
|
|
err := r.db.Location.Query().
|
|
Where(
|
|
location.HasGroupWith(group.ID(GID)),
|
|
).
|
|
GroupBy(location.FieldID, location.FieldName).
|
|
Aggregate(func(sq *sql.Selector) string {
|
|
t := sql.Table(item.Table)
|
|
sq.Join(t).On(sq.C(location.FieldID), t.C(item.LocationColumn))
|
|
|
|
return sql.As(sql.Sum(t.C(item.FieldPurchasePrice)), "total")
|
|
}).
|
|
Scan(ctx, &v)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return v, err
|
|
}
|
|
|
|
func (r *GroupRepository) StatsLabelsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) {
|
|
var v []TotalsByOrganizer
|
|
|
|
err := r.db.Label.Query().
|
|
Where(
|
|
label.HasGroupWith(group.ID(GID)),
|
|
).
|
|
GroupBy(label.FieldID, label.FieldName).
|
|
Aggregate(func(sq *sql.Selector) string {
|
|
itemTable := sql.Table(item.Table)
|
|
|
|
jt := sql.Table(label.ItemsTable)
|
|
|
|
sq.Join(jt).On(sq.C(label.FieldID), jt.C(label.ItemsPrimaryKey[0]))
|
|
sq.Join(itemTable).On(jt.C(label.ItemsPrimaryKey[1]), itemTable.C(item.FieldID))
|
|
|
|
return sql.As(sql.Sum(itemTable.C(item.FieldPurchasePrice)), "total")
|
|
}).
|
|
Scan(ctx, &v)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return v, err
|
|
}
|
|
|
|
func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, GID uuid.UUID, start, end time.Time) (*ValueOverTime, error) {
|
|
// Get the Totals for the Start and End of the Given Time Period
|
|
q := `
|
|
SELECT
|
|
(SELECT Sum(purchase_price)
|
|
FROM items
|
|
WHERE group_items = ?
|
|
AND items.archived = false
|
|
AND items.created_at < ?) AS price_at_start,
|
|
(SELECT Sum(purchase_price)
|
|
FROM items
|
|
WHERE group_items = ?
|
|
AND items.archived = false
|
|
AND items.created_at < ?) AS price_at_end
|
|
`
|
|
stats := ValueOverTime{
|
|
Start: start,
|
|
End: end,
|
|
}
|
|
|
|
var maybeStart *float64
|
|
var maybeEnd *float64
|
|
|
|
row := r.db.Sql().QueryRowContext(ctx, q, GID, sqliteDateFormat(start), GID, sqliteDateFormat(end))
|
|
err := row.Scan(&maybeStart, &maybeEnd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.PriceAtStart = orDefault(maybeStart, 0)
|
|
stats.PriceAtEnd = orDefault(maybeEnd, 0)
|
|
|
|
var v []struct {
|
|
Name string `json:"name"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
PurchasePrice float64 `json:"purchase_price"`
|
|
}
|
|
|
|
// Get Created Date and Price of all items between start and end
|
|
err = r.db.Item.Query().
|
|
Where(
|
|
item.HasGroupWith(group.ID(GID)),
|
|
item.CreatedAtGTE(start),
|
|
item.CreatedAtLTE(end),
|
|
item.Archived(false),
|
|
).
|
|
Select(
|
|
item.FieldName,
|
|
item.FieldCreatedAt,
|
|
item.FieldPurchasePrice,
|
|
).
|
|
Scan(ctx, &v)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Entries = make([]ValueOverTimeEntry, len(v))
|
|
|
|
for i, vv := range v {
|
|
stats.Entries[i] = ValueOverTimeEntry{
|
|
Date: vv.CreatedAt,
|
|
Value: vv.PurchasePrice,
|
|
}
|
|
}
|
|
|
|
return &stats, nil
|
|
}
|
|
|
|
func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupStatistics, error) {
|
|
q := `
|
|
SELECT
|
|
(SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users,
|
|
(SELECT COUNT(*) FROM items WHERE group_items = ? AND items.archived = false) AS total_items,
|
|
(SELECT COUNT(*) FROM locations WHERE group_locations = ?) AS total_locations,
|
|
(SELECT COUNT(*) FROM labels WHERE group_labels = ?) AS total_labels,
|
|
(SELECT SUM(purchase_price) FROM items WHERE group_items = ? AND items.archived = false) AS total_item_price,
|
|
(SELECT COUNT(*)
|
|
FROM items
|
|
WHERE group_items = ?
|
|
AND items.archived = false
|
|
AND (items.lifetime_warranty = true OR items.warranty_expires > date())
|
|
) AS total_with_warranty
|
|
`
|
|
var stats GroupStatistics
|
|
row := r.db.Sql().QueryRowContext(ctx, q, GID, GID, GID, GID, GID, GID)
|
|
|
|
err := row.Scan(&stats.TotalUsers, &stats.TotalItems, &stats.TotalLocations, &stats.TotalLabels, &stats.TotalItemPrice, &stats.TotalWithWarranty)
|
|
if err != nil {
|
|
return GroupStatistics{}, err
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) {
|
|
return mapToGroupErr(r.db.Group.Create().
|
|
SetName(name).
|
|
Save(ctx))
|
|
}
|
|
|
|
func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data GroupUpdate) (Group, error) {
|
|
currency := group.Currency(strings.ToLower(data.Currency))
|
|
|
|
entity, err := r.db.Group.UpdateOneID(ID).
|
|
SetName(data.Name).
|
|
SetCurrency(currency).
|
|
Save(ctx)
|
|
|
|
return mapToGroupErr(entity, err)
|
|
}
|
|
|
|
func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
|
|
return mapToGroupErr(r.db.Group.Get(ctx, id))
|
|
}
|
|
|
|
func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) {
|
|
return mapToGroupInvitationErr(r.db.GroupInvitationToken.Query().
|
|
Where(groupinvitationtoken.Token(token)).
|
|
WithGroup().
|
|
Only(ctx))
|
|
}
|
|
|
|
func (r *GroupRepository) InvitationCreate(ctx context.Context, groupID uuid.UUID, invite GroupInvitationCreate) (GroupInvitation, error) {
|
|
entity, err := r.db.GroupInvitationToken.Create().
|
|
SetGroupID(groupID).
|
|
SetToken(invite.Token).
|
|
SetExpiresAt(invite.ExpiresAt).
|
|
SetUses(invite.Uses).
|
|
Save(ctx)
|
|
|
|
if err != nil {
|
|
return GroupInvitation{}, err
|
|
}
|
|
|
|
return r.InvitationGet(ctx, entity.Token)
|
|
}
|
|
|
|
func (r *GroupRepository) InvitationUpdate(ctx context.Context, id uuid.UUID, uses int) error {
|
|
_, err := r.db.GroupInvitationToken.UpdateOneID(id).SetUses(uses).Save(ctx)
|
|
return err
|
|
}
|
|
|
|
// InvitationPurge removes all expired invitations or those that have been used up.
|
|
// It returns the number of deleted invitations.
|
|
func (r *GroupRepository) InvitationPurge(ctx context.Context) (amount int, err error) {
|
|
q := r.db.GroupInvitationToken.Delete()
|
|
q.Where(groupinvitationtoken.Or(
|
|
groupinvitationtoken.ExpiresAtLT(time.Now()),
|
|
groupinvitationtoken.UsesLTE(0),
|
|
))
|
|
|
|
return q.Exec(ctx)
|
|
}
|