|
| 1 | +import Foundation |
| 2 | +import protocol Storage.GRDBManagerProtocol |
| 3 | +import class WooFoundation.CurrencySettings |
| 4 | + |
| 5 | +/// Service for handling barcode scanning using local GRDB catalog |
| 6 | +public final class PointOfSaleLocalBarcodeScanService: PointOfSaleBarcodeScanServiceProtocol { |
| 7 | + private let grdbManager: GRDBManagerProtocol |
| 8 | + private let siteID: Int64 |
| 9 | + private let itemMapper: PointOfSaleItemMapperProtocol |
| 10 | + |
| 11 | + public init(siteID: Int64, |
| 12 | + grdbManager: GRDBManagerProtocol, |
| 13 | + currencySettings: CurrencySettings, |
| 14 | + itemMapper: PointOfSaleItemMapperProtocol? = nil) { |
| 15 | + self.siteID = siteID |
| 16 | + self.grdbManager = grdbManager |
| 17 | + self.itemMapper = itemMapper ?? PointOfSaleItemMapper(currencySettings: currencySettings) |
| 18 | + } |
| 19 | + |
| 20 | + /// Looks up a POSItem using a barcode scan string from the local GRDB catalog |
| 21 | + /// - Parameter barcode: The barcode string from a scan (global unique identifier) |
| 22 | + /// - Returns: A POSItem if found, or throws an error |
| 23 | + public func getItem(barcode: String) async throws(PointOfSaleBarcodeScanError) -> POSItem { |
| 24 | + do { |
| 25 | + if let product = try searchProductByGlobalUniqueID(barcode) { |
| 26 | + return try convertProductToItem(product, scannedCode: barcode) |
| 27 | + } |
| 28 | + |
| 29 | + if let variationAndParent = try searchVariationByGlobalUniqueID(barcode) { |
| 30 | + return try convertVariationToItem(variationAndParent.variation, parentProduct: variationAndParent.parentProduct, scannedCode: barcode) |
| 31 | + } |
| 32 | + |
| 33 | + throw PointOfSaleBarcodeScanError.notFound(scannedCode: barcode) |
| 34 | + } catch let error as PointOfSaleBarcodeScanError { |
| 35 | + throw error |
| 36 | + } catch { |
| 37 | + throw PointOfSaleBarcodeScanError.loadingError(scannedCode: barcode, underlyingError: error) |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + // MARK: - Product Search |
| 42 | + |
| 43 | + private func searchProductByGlobalUniqueID(_ globalUniqueID: String) throws -> PersistedProduct? { |
| 44 | + try grdbManager.databaseConnection.read { db in |
| 45 | + try PersistedProduct.posProductByGlobalUniqueID(siteID: siteID, globalUniqueID: globalUniqueID).fetchOne(db) |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + // MARK: - Variation Search |
| 50 | + |
| 51 | + private func searchVariationByGlobalUniqueID(_ globalUniqueID: String) throws -> (variation: PersistedProductVariation, parentProduct: PersistedProduct)? { |
| 52 | + try grdbManager.databaseConnection.read { db in |
| 53 | + guard let variation = try PersistedProductVariation.posVariationByGlobalUniqueID(siteID: siteID, globalUniqueID: globalUniqueID).fetchOne(db) else { |
| 54 | + return nil |
| 55 | + } |
| 56 | + // Fetch parent product using the relationship |
| 57 | + guard let parentProduct = try variation.request(for: PersistedProductVariation.parentProduct).fetchOne(db) else { |
| 58 | + throw PointOfSaleBarcodeScanError.noParentProductForVariation(scannedCode: globalUniqueID) |
| 59 | + } |
| 60 | + return (variation, parentProduct) |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + // MARK: - Conversion to POSItem |
| 65 | + |
| 66 | + private func convertProductToItem(_ persistedProduct: PersistedProduct, scannedCode: String) throws(PointOfSaleBarcodeScanError) -> POSItem { |
| 67 | + do { |
| 68 | + let posProduct = try persistedProduct.toPOSProduct(db: grdbManager.databaseConnection) |
| 69 | + |
| 70 | + guard !posProduct.downloadable else { |
| 71 | + throw PointOfSaleBarcodeScanError.downloadableProduct(scannedCode: scannedCode, productName: posProduct.name) |
| 72 | + } |
| 73 | + |
| 74 | + // Validate product type - only simple products can be scanned directly |
| 75 | + // Variable parent products cannot be added to cart (only their variations can) |
| 76 | + guard posProduct.productType == .simple else { |
| 77 | + throw PointOfSaleBarcodeScanError.unsupportedProductType( |
| 78 | + scannedCode: scannedCode, |
| 79 | + productName: posProduct.name, |
| 80 | + productType: posProduct.productType |
| 81 | + ) |
| 82 | + } |
| 83 | + |
| 84 | + // Convert to POSItem |
| 85 | + let items = itemMapper.mapProductsToPOSItems(products: [posProduct]) |
| 86 | + guard let item = items.first else { |
| 87 | + throw PointOfSaleBarcodeScanError.unknown(scannedCode: scannedCode) |
| 88 | + } |
| 89 | + |
| 90 | + return item |
| 91 | + } catch let error as PointOfSaleBarcodeScanError { |
| 92 | + throw error |
| 93 | + } catch { |
| 94 | + throw PointOfSaleBarcodeScanError.mappingError(scannedCode: scannedCode, underlyingError: error) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private func convertVariationToItem(_ persistedVariation: PersistedProductVariation, |
| 99 | + parentProduct: PersistedProduct, |
| 100 | + scannedCode: String) throws(PointOfSaleBarcodeScanError) -> POSItem { |
| 101 | + do { |
| 102 | + // Convert both variation and parent to POS models |
| 103 | + let posVariation = try persistedVariation.toPOSProductVariation(db: grdbManager.databaseConnection) |
| 104 | + let parentPOSProduct = try parentProduct.toPOSProduct(db: grdbManager.databaseConnection) |
| 105 | + |
| 106 | + // Map to POSItem |
| 107 | + guard let mappedParent = itemMapper.mapProductsToPOSItems(products: [parentPOSProduct]).first, |
| 108 | + case .variableParentProduct(let variableParentProduct) = mappedParent, |
| 109 | + let item = itemMapper.mapVariationsToPOSItems(variations: [posVariation], parentProduct: variableParentProduct).first else { |
| 110 | + throw PointOfSaleBarcodeScanError.variationCouldNotBeConverted(scannedCode: scannedCode) |
| 111 | + } |
| 112 | + |
| 113 | + guard !persistedVariation.downloadable else { |
| 114 | + throw PointOfSaleBarcodeScanError.downloadableProduct(scannedCode: scannedCode, |
| 115 | + productName: variationName(for: item)) |
| 116 | + } |
| 117 | + |
| 118 | + return item |
| 119 | + } catch let error as PointOfSaleBarcodeScanError { |
| 120 | + throw error |
| 121 | + } catch { |
| 122 | + throw PointOfSaleBarcodeScanError.mappingError(scannedCode: scannedCode, underlyingError: error) |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + private func variationName(for item: POSItem) -> String { |
| 127 | + guard case .variation(let posVariation) = item else { |
| 128 | + return Localization.unknownVariationName |
| 129 | + } |
| 130 | + return posVariation.name |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +private extension PointOfSaleLocalBarcodeScanService { |
| 135 | + enum Localization { |
| 136 | + static let unknownVariationName = NSLocalizedString( |
| 137 | + "pointOfSale.barcodeScanning.unresolved.variation.name", |
| 138 | + value: "Unknown", |
| 139 | + comment: "A placeholder name when we can't determine the name of a variation for an error message") |
| 140 | + } |
| 141 | +} |
0 commit comments