import { Injectable, NotFoundException, UnauthorizedException, } from '@nestjs/common'; import omit from 'lodash.omit'; import pick from 'lodash.pick'; import { BuildingService } from 'src/objects/building/building.service'; import { Room } from 'src/objects/building/entities/room.entity'; import { StoredItemTransaction } from 'src/objects/storage/entities/item-transaction.entity'; import { Item } from 'src/objects/storage/entities/item.entity'; import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; import { Storage } from 'src/objects/storage/entities/storage.entity'; import { StoredItem } from 'src/objects/storage/entities/stored-item.entity'; import { TransactionType } from 'src/objects/storage/enums/transaction-type.enum'; import { StorageService } from 'src/objects/storage/storage.service'; import { User } from 'src/objects/user/user.entity'; import { StorageAddExistingItemRequestDto, StorageAddItemRequestDto, StorageItemUpdateRequestDto, StorageStoredItemRequestDto, StorageStoredItemTransactionRequestDto, StorageStoredItemTransactionUpdateRequestDto, StorageStoredItemUpdateRequestDto, } from './dto/storage-add-item-request.dto'; import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; import { StorageItemResponseDto, StorageItemSearchResponseDto, StorageStoredItemResponseDto, StorageTransactionResponseDto, StorageTransactionWithActorResponseDto, } from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, StorageUpdateRequestDto, } from './dto/storage-request.dto'; import { StorageActorResponse, StorageResponseDto, StorageResponseWithItemCountDto, } from './dto/storage-response.dto'; import { StorageSetCreateRequestDto, StorageSetUpdateRequestDto, } from './dto/storage-set-request.dto'; import { StorageSetResponseDto } from './dto/storage-set-response.dto'; @Injectable() export class AppStorageService { constructor( private readonly buildingService: BuildingService, private readonly storageService: StorageService, ) {} async getStoragesInRoom(roomId: number, includeWithSets = false) { const storages = await this.storageService.getStoragesInRoom( roomId, includeWithSets, ); return storages.map((storage) => this.formatStorageNoItems(storage)); } async getStorageSetsInRoom(roomId: number) { const sets = await this.storageService.getStorageSetsInRoom(roomId); return sets.map((set) => this.formatStorageSetNoItems(set)); } async createStorage(user: User, room: Room, body: StorageCreateRequestDto) { // TODO: validate color and location const newStorage = await this.storageService.saveStorage({ ...body, addedBy: user, room, }); return this.formatStorageNoItems(newStorage); } async createStorageSet( user: User, room: Room, body: StorageSetCreateRequestDto, ) { // TODO: validate color and location const newStorageSet = await this.storageService.saveStorageSet({ ...body, addedBy: user, room, }); return this.formatStorageSetNoItems(newStorageSet); } async updateStorage(storage: Storage, body: StorageUpdateRequestDto) { Object.assign(storage, body); await this.storageService.saveStorage(storage); return this.formatStorageNoItems(storage); } async updateStorageSet( storageSet: StorageSet, body: StorageSetUpdateRequestDto, ) { Object.assign(storageSet, body); await this.storageService.saveStorageSet(storageSet); return this.formatStorageSetNoItems(storageSet); } async moveStorage(set: StorageSet, storage: Storage) { storage.set = set; await this.storageService.saveStorage(storage); set.storages = [...(set.storages || []), storage]; return this.formatStorageSetNoItems(set); } async removeFromSet(set: StorageSet, storage: Storage) { storage.set = null; await this.storageService.saveStorage(storage); set.storages = (set.storages || []).filter( (entry) => entry.id !== storage.id, ); return this.formatStorageSetNoItems(set); } async searchForItems(user: User, query: StorageItemRequestQueryDto) { let responses: StorageItemSearchResponseDto[]; if (query.searchTerm) { responses = await this.storageService.searchForItem( query.searchTerm, user.sub, ); } else { responses = await this.storageService.searchForItemByBarcode( query.barcode, user.sub, ); } return responses; } async getExpiringOrExpiredItemsInBuilding(buildingId: number) { const expiringSoon = await this.storageService.getExpiredOrExpiringSoonInBuilding(buildingId); return expiringSoon.map((storedItem) => this.formatStoredItem(storedItem, true), ); } async getExpiringOrExpiredItems(user: User) { const expiringSoon = await this.storageService.getExpiredOrExpiringSoonForSub(user.sub); return expiringSoon.map((storedItem) => this.formatStoredItem(storedItem, true), ); } async createStoredItem( user: User, item: Item, storage: Storage, transactionInfo: StorageStoredItemTransactionRequestDto, additionalInfo?: StorageStoredItemRequestDto, ) { const building = await this.buildingService.getStorageBuilding(storage.id); if (!building) throw new NotFoundException('Building for storage was not found'); // Create stored item let storedItem = new StoredItem(); storedItem.addedBy = user; storedItem.item = item; storedItem.storage = storage; storedItem.building = building; additionalInfo && Object.assign(storedItem, additionalInfo); storedItem = await this.storageService.saveStoredItem(storedItem); // Create transaction let transaction = new StoredItemTransaction(); transaction.actor = user; transaction.storedItem = storedItem; Object.assign(transaction, transactionInfo); transaction = await this.storageService.saveStoredItemTransaction( transaction, ); storedItem.transactions = [transaction]; return new StorageStoredItemResponseDto(this.formatStoredItem(storedItem)); } async createNewItem( user: User, storage: Storage, body: StorageAddItemRequestDto, ) { // Create item let item = new Item(); item.addedBy = user; Object.assign( item, pick(body, [ 'displayName', 'type', 'barcode', 'consumable', 'image', 'weight', 'url', 'notes', 'public', ]), ); item = await this.storageService.saveItem(item); return this.createStoredItem( user, item, storage, body.transactionInfo, body.additionalInfo, ); } async addExistingItemToStorage( user: User, storage: Storage, itemId: number, body: StorageAddExistingItemRequestDto, ) { const item = await this.storageService.getItemByIdBySub(itemId, user.sub); if (!item) { throw new NotFoundException('Item not found'); } return this.createStoredItem( user, item, storage, body.transactionInfo, body.additionalInfo, ); } async updateOwnedItem( user: User, itemId: number, body: StorageItemUpdateRequestDto, ) { const item = await this.storageService.getItemByIdOwnedBySub( itemId, user.sub, ); if (!item) { throw new NotFoundException('Item not found'); } Object.assign(item, body); await this.storageService.saveItem(item); return this.formatItem(item); } async updateStoredItemDetails( storage: Storage, storedItemId: number, body: StorageStoredItemUpdateRequestDto, ) { const storedItem = await this.storageService.getStoredItemByStorageAndId( storage, storedItemId, ); if (!storedItem) { throw new NotFoundException('Stored item not found'); } Object.assign(storedItem, body); await this.storageService.saveStoredItem(storedItem); return this.formatStoredItem(storedItem); } async createStoredItemTransaction( user: User, storage: Storage, storedItemId: number, body: StorageStoredItemTransactionRequestDto, ) { const storedItem = await this.storageService.getStoredItemByStorageAndId( storage, storedItemId, ); if (!storedItem) { throw new NotFoundException('Stored item not found'); } const transaction = await this.storageService.saveStoredItemTransaction({ ...body, actor: user, storedItem, }); if ( [ TransactionType.SOLD, TransactionType.DESTROYED, TransactionType.BINNED, ].includes(body.type) ) { storedItem.consumedAt = new Date(); await this.storageService.saveStoredItem(storedItem); } return this.formatTransaction(transaction); } async updateStoredItemTransaction( user: User, storedItemId: number, transactionId: number, body: StorageStoredItemTransactionUpdateRequestDto, ) { const storedItemTransaction = await this.storageService.getStoredItemTransactionById( storedItemId, transactionId, ['actor'], ); if (!storedItemTransaction) { throw new NotFoundException('Stored item not found'); } if (storedItemTransaction.actor.sub === user.sub) { throw new UnauthorizedException( 'You can only update your own transactions', ); } Object.assign(storedItemTransaction, body); await this.storageService.saveStoredItemTransaction(storedItemTransaction); return this.formatTransactionWithActor(storedItemTransaction); } async getStoredItemTransaction(storage: Storage, storedItemId: number) { const storedItem = await this.storageService.getStoredItemByStorageAndId( storage, storedItemId, ['transactions', 'transactions.actor'], ); if (!storedItem) { throw new NotFoundException('Stored item not found'); } return storedItem.transactions.map((transaction) => this.formatTransactionWithActor(transaction), ); } async getStoredItemDetails(storage: Storage, storedItemId: number) { const storedItem = await this.storageService.getStoredItemByStorageAndId( storage, storedItemId, ['item', 'addedBy'], ); if (!storedItem) { throw new NotFoundException('Stored item not found'); } return this.formatStoredItem(storedItem); } async getStorageWithItems(storage: Storage) { const withItemCount = await this.storageService.getStorageWithItemCount( storage.id, ); return this.formatStorageNoItems( withItemCount, ) as StorageResponseWithItemCountDto; } private formatActor(input: User): StorageActorResponse { return pick(input, ['name', 'sub', 'picture', 'color']); } private formatStorageNoItems(storage: Storage): StorageResponseDto { return { ...omit(storage, ['room', 'items']), addedBy: storage.addedBy && this.formatActor(storage.addedBy), }; } private formatStorageWithItems(storage: Storage): StorageResponseDto { return { ...omit(storage, ['room', 'set']), items: !!storage.items?.length ? storage.items.map((item) => this.formatStoredItem(item)) : null, addedBy: storage.addedBy && this.formatActor(storage.addedBy), }; } private formatItem(item: Item): StorageItemResponseDto { return { ...omit(item, ['instances', 'addedBy']), addedBy: item.addedBy && this.formatActor(item.addedBy), }; } private formatTransaction( transaction: StoredItemTransaction, ): StorageTransactionResponseDto { return omit(transaction, ['storedItem', 'actor']); } private formatTransactionWithActor( transaction: StoredItemTransaction, ): StorageTransactionWithActorResponseDto { return { ...omit(transaction, ['storedItem']), actor: transaction.actor && this.formatActor(transaction.actor), }; } private formatStoredItem( storedItem: StoredItem, fullSet = false, ): StorageStoredItemResponseDto { return { ...(fullSet ? storedItem : omit(storedItem, ['storage', 'building'])), transactions: !!storedItem.transactions?.length ? storedItem.transactions.map((transaction) => this.formatTransaction(transaction), ) : null, item: storedItem.item ? this.formatItem(storedItem.item) : null, addedBy: storedItem.addedBy ? this.formatActor(storedItem.addedBy) : null, }; } formatStorageSetNoItems(set: StorageSet): StorageSetResponseDto { return { ...omit(set, ['room']), addedBy: set.addedBy && this.formatActor(set.addedBy), storages: set.storages?.length ? set.storages.map((storage) => this.formatStorageNoItems(storage)) : [], }; } }