diff --git a/client/packages/coldchain/src/Equipment/ListView/AddFromScannerButton.tsx b/client/packages/coldchain/src/Equipment/ListView/AddFromScannerButton.tsx index 75bb896ead..0c6bbd333d 100644 --- a/client/packages/coldchain/src/Equipment/ListView/AddFromScannerButton.tsx +++ b/client/packages/coldchain/src/Equipment/ListView/AddFromScannerButton.tsx @@ -34,7 +34,8 @@ export const AddFromScannerButtonComponent = () => { const equipmentRoute = RouteBuilder.create(AppRoute.Coldchain).addPart( AppRoute.Equipment ); - const { mutateAsync: scanAsset } = useAssets.document.scan(); + const { mutateAsync: fetchAsset } = useAssets.document.fetch(); + const { mutateAsync: fetchGS1 } = useAssets.document.gs1(); const { mutateAsync: saveNewAsset } = useAssets.document.insert(); const { insertLog, invalidateQueries } = useAssets.log.insert(); const newAssetData = useRef(); @@ -64,9 +65,24 @@ export const AddFromScannerButtonComponent = () => { const handleScanResult = async (result: ScanResult) => { if (!!result.content) { - const { content } = result; + const { gs1 } = result; - const asset = await scanAsset(content).catch(() => {}); + if (!gs1) { + // try to fetch the asset by id, as it could be an id from our own barcode + const { content } = result; + const id = content; + const asset = await fetchAsset(id).catch(() => {}); + if (asset) { + navigate(equipmentRoute.addPart(id).build()); + + return; + } + error(t('error.no-matching-asset', { id }))(); + return; + } + + // send the GS1 data to backend to handle + const asset = await fetchGS1(gs1).catch(() => {}); if (asset?.__typename !== 'AssetNode') { error(t('error.no-matching-asset', { id: result.content }))(); diff --git a/client/packages/coldchain/src/Equipment/api/api.ts b/client/packages/coldchain/src/Equipment/api/api.ts index 90779d2b9c..9b3cf12e4a 100644 --- a/client/packages/coldchain/src/Equipment/api/api.ts +++ b/client/packages/coldchain/src/Equipment/api/api.ts @@ -7,10 +7,12 @@ import { setNullableInput, InsertAssetLogInput, AssetLogSortFieldInput, + Gs1DataElement, } from '@openmsupply-client/common'; import { Sdk, AssetFragment } from './operations.generated'; import { CCE_CLASS_ID } from '../utils'; import { DraftAsset } from '../types'; +import { Gs1Barcode } from 'gs1-barcode-parser-mod'; export type ListParams = { first: number; @@ -97,12 +99,15 @@ export const getAssetQueries = (sdk: Sdk, storeId: string) => ({ throw new Error('Asset not found'); }, - byScannerString: async (inputText: string) => { - const { assetByScannedString } = await sdk.assetByScannedString({ + byGs1Elements: async (data: Gs1Barcode) => { + let dataElements: Gs1DataElement[] = data.parsedCodeItems.map(item => { + return { ai: item.ai, data: item.data.toString() }; + }); + const { assetFromGs1Data } = await sdk.assetFromGs1Data({ storeId, - inputText, + data: dataElements, }); - return assetByScannedString; + return assetFromGs1Data; }, list: async ( { first, offset, sortBy, filterBy }: ListParams, diff --git a/client/packages/coldchain/src/Equipment/api/hooks/document/index.ts b/client/packages/coldchain/src/Equipment/api/hooks/document/index.ts index e17981dffc..d2f060c7b2 100644 --- a/client/packages/coldchain/src/Equipment/api/hooks/document/index.ts +++ b/client/packages/coldchain/src/Equipment/api/hooks/document/index.ts @@ -1,8 +1,4 @@ -import { - useAsset, - useFetchAssetById, - useFetchAssetByScannerString, -} from './useAsset'; +import { useAsset, useFetchAssetById, useFetchAssetByGS1 } from './useAsset'; import { useAssetDelete } from './useAssetDelete'; import { useAssetFields } from './useAssetFields'; import { useAssetInsert } from './useAssetInsert'; @@ -22,6 +18,6 @@ export const Document = { useAssetsDelete, useAssetUpdate, useFetchAssetById, - useFetchAssetByScannerString, + useFetchAssetByGS1, useAssetProperties, }; diff --git a/client/packages/coldchain/src/Equipment/api/hooks/document/useAsset.ts b/client/packages/coldchain/src/Equipment/api/hooks/document/useAsset.ts index 08a2d6de5d..13e14b817f 100644 --- a/client/packages/coldchain/src/Equipment/api/hooks/document/useAsset.ts +++ b/client/packages/coldchain/src/Equipment/api/hooks/document/useAsset.ts @@ -29,9 +29,9 @@ export const useFetchAssetById = () => { }); }; -export const useFetchAssetByScannerString = () => { +export const useFetchAssetByGS1 = () => { const api = useAssetApi(); - return useMutation(api.get.byScannerString, { + return useMutation(api.get.byGs1Elements, { onError: () => {}, }); }; diff --git a/client/packages/coldchain/src/Equipment/api/hooks/index.ts b/client/packages/coldchain/src/Equipment/api/hooks/index.ts index 1a201c55e6..ac6ffce3a8 100644 --- a/client/packages/coldchain/src/Equipment/api/hooks/index.ts +++ b/client/packages/coldchain/src/Equipment/api/hooks/index.ts @@ -10,7 +10,7 @@ export const useAssets = { document: { fetch: Document.useFetchAssetById, - scan: Document.useFetchAssetByScannerString, + gs1: Document.useFetchAssetByGS1, get: Document.useAsset, list: Document.useAssets, listAll: Document.useAssetsAll, diff --git a/client/packages/coldchain/src/Equipment/api/operations.generated.ts b/client/packages/coldchain/src/Equipment/api/operations.generated.ts index 2ea9ce7689..793ead4379 100644 --- a/client/packages/coldchain/src/Equipment/api/operations.generated.ts +++ b/client/packages/coldchain/src/Equipment/api/operations.generated.ts @@ -29,13 +29,13 @@ export type AssetByIdQueryVariables = Types.Exact<{ export type AssetByIdQuery = { __typename: 'Queries', assets: { __typename: 'AssetConnector', totalCount: number, nodes: Array<{ __typename: 'AssetNode', catalogueItemId?: string | null, assetNumber?: string | null, createdDatetime: any, id: string, installationDate?: string | null, properties: string, catalogProperties?: string | null, modifiedDatetime: any, notes?: string | null, replacementDate?: string | null, serialNumber?: string | null, storeId?: string | null, donorNameId?: string | null, warrantyStart?: string | null, warrantyEnd?: string | null, needsReplacement?: boolean | null, documents: { __typename: 'SyncFileReferenceConnector', nodes: Array<{ __typename: 'SyncFileReferenceNode', fileName: string, id: string, mimeType?: string | null }> }, locations: { __typename: 'LocationConnector', totalCount: number, nodes: Array<{ __typename: 'LocationNode', id: string, code: string, name: string, onHold: boolean, coldStorageType?: { __typename: 'ColdStorageTypeNode', id: string, name: string, maxTemperature: number, minTemperature: number } | null }> }, statusLog?: { __typename: 'AssetLogNode', logDatetime: any, status?: Types.StatusType | null, reason?: { __typename: 'AssetLogReasonNode', reason: string } | null } | null, store?: { __typename: 'StoreNode', id: string, code: string, storeName: string } | null, catalogueItem?: { __typename: 'AssetCatalogueItemNode', manufacturer?: string | null, model: string } | null, assetType?: { __typename: 'AssetTypeNode', id: string, name: string } | null, assetClass?: { __typename: 'AssetClassNode', id: string, name: string } | null, assetCategory?: { __typename: 'AssetCategoryNode', id: string, name: string } | null, donor?: { __typename: 'NameNode', id: string, name: string } | null }> } }; -export type AssetByScannedStringQueryVariables = Types.Exact<{ +export type AssetFromGs1DataQueryVariables = Types.Exact<{ storeId: Types.Scalars['String']['input']; - inputText: Types.Scalars['String']['input']; + data: Array | Types.Gs1DataElement; }>; -export type AssetByScannedStringQuery = { __typename: 'Queries', assetByScannedString: { __typename: 'AssetNode', catalogueItemId?: string | null, assetNumber?: string | null, createdDatetime: any, id: string, installationDate?: string | null, properties: string, catalogProperties?: string | null, modifiedDatetime: any, notes?: string | null, replacementDate?: string | null, serialNumber?: string | null, storeId?: string | null, donorNameId?: string | null, warrantyStart?: string | null, warrantyEnd?: string | null, needsReplacement?: boolean | null, documents: { __typename: 'SyncFileReferenceConnector', nodes: Array<{ __typename: 'SyncFileReferenceNode', fileName: string, id: string, mimeType?: string | null }> }, locations: { __typename: 'LocationConnector', totalCount: number, nodes: Array<{ __typename: 'LocationNode', id: string, code: string, name: string, onHold: boolean, coldStorageType?: { __typename: 'ColdStorageTypeNode', id: string, name: string, maxTemperature: number, minTemperature: number } | null }> }, statusLog?: { __typename: 'AssetLogNode', logDatetime: any, status?: Types.StatusType | null, reason?: { __typename: 'AssetLogReasonNode', reason: string } | null } | null, store?: { __typename: 'StoreNode', id: string, code: string, storeName: string } | null, catalogueItem?: { __typename: 'AssetCatalogueItemNode', manufacturer?: string | null, model: string } | null, assetType?: { __typename: 'AssetTypeNode', id: string, name: string } | null, assetClass?: { __typename: 'AssetClassNode', id: string, name: string } | null, assetCategory?: { __typename: 'AssetCategoryNode', id: string, name: string } | null, donor?: { __typename: 'NameNode', id: string, name: string } | null } | { __typename: 'ScannedDataParseError' } }; +export type AssetFromGs1DataQuery = { __typename: 'Queries', assetFromGs1Data: { __typename: 'AssetNode', catalogueItemId?: string | null, assetNumber?: string | null, createdDatetime: any, id: string, installationDate?: string | null, properties: string, catalogProperties?: string | null, modifiedDatetime: any, notes?: string | null, replacementDate?: string | null, serialNumber?: string | null, storeId?: string | null, donorNameId?: string | null, warrantyStart?: string | null, warrantyEnd?: string | null, needsReplacement?: boolean | null, documents: { __typename: 'SyncFileReferenceConnector', nodes: Array<{ __typename: 'SyncFileReferenceNode', fileName: string, id: string, mimeType?: string | null }> }, locations: { __typename: 'LocationConnector', totalCount: number, nodes: Array<{ __typename: 'LocationNode', id: string, code: string, name: string, onHold: boolean, coldStorageType?: { __typename: 'ColdStorageTypeNode', id: string, name: string, maxTemperature: number, minTemperature: number } | null }> }, statusLog?: { __typename: 'AssetLogNode', logDatetime: any, status?: Types.StatusType | null, reason?: { __typename: 'AssetLogReasonNode', reason: string } | null } | null, store?: { __typename: 'StoreNode', id: string, code: string, storeName: string } | null, catalogueItem?: { __typename: 'AssetCatalogueItemNode', manufacturer?: string | null, model: string } | null, assetType?: { __typename: 'AssetTypeNode', id: string, name: string } | null, assetClass?: { __typename: 'AssetClassNode', id: string, name: string } | null, assetCategory?: { __typename: 'AssetCategoryNode', id: string, name: string } | null, donor?: { __typename: 'NameNode', id: string, name: string } | null } | { __typename: 'ScannedDataParseError' } }; export type AssetLogsQueryVariables = Types.Exact<{ filter: Types.AssetLogFilterInput; @@ -250,9 +250,9 @@ export const AssetByIdDocument = gql` } } ${AssetFragmentDoc}`; -export const AssetByScannedStringDocument = gql` - query assetByScannedString($storeId: String!, $inputText: String!) { - assetByScannedString(storeId: $storeId, inputText: $inputText) { +export const AssetFromGs1DataDocument = gql` + query assetFromGs1Data($storeId: String!, $data: [Gs1DataElement!]!) { + assetFromGs1Data(storeId: $storeId, gs1: $data) { ... on AssetNode { ...Asset } @@ -363,8 +363,8 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = assetById(variables: AssetByIdQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(AssetByIdDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'assetById', 'query', variables); }, - assetByScannedString(variables: AssetByScannedStringQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(AssetByScannedStringDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'assetByScannedString', 'query', variables); + assetFromGs1Data(variables: AssetFromGs1DataQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(AssetFromGs1DataDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'assetFromGs1Data', 'query', variables); }, assetLogs(variables: AssetLogsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(AssetLogsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'assetLogs', 'query', variables); diff --git a/client/packages/coldchain/src/Equipment/api/operations.graphql b/client/packages/coldchain/src/Equipment/api/operations.graphql index 249ebaebc7..575022b7b1 100644 --- a/client/packages/coldchain/src/Equipment/api/operations.graphql +++ b/client/packages/coldchain/src/Equipment/api/operations.graphql @@ -167,8 +167,8 @@ query assetById($storeId: String!, $assetId: String!) { } } -query assetByScannedString($storeId: String!, $inputText: String!) { - assetByScannedString(storeId: $storeId, inputText: $inputText) { +query assetFromGs1Data($storeId: String!, $data: [Gs1DataElement!]!) { + assetFromGs1Data(storeId: $storeId, gs1: $data) { ... on AssetNode { ...Asset } diff --git a/client/packages/common/src/types/schema.ts b/client/packages/common/src/types/schema.ts index 9cab1ca79b..4eeafe01c0 100644 --- a/client/packages/common/src/types/schema.ts +++ b/client/packages/common/src/types/schema.ts @@ -2344,6 +2344,11 @@ export type GeneratedCustomerReturnLineConnector = { totalCount: Scalars['Int']['output']; }; +export type Gs1DataElement = { + ai: Scalars['String']['input']; + data: Scalars['String']['input']; +}; + export type InboundInvoiceCounts = { __typename: 'InboundInvoiceCounts'; created: InvoiceCountsSummary; @@ -5457,13 +5462,13 @@ export type Queries = { activeProgramEvents: ProgramEventResponse; activityLogs: ActivityLogResponse; apiVersion: Scalars['String']['output']; - assetByScannedString: AssetParseResponse; assetCatalogueItem: AssetCatalogueItemResponse; assetCatalogueItems: AssetCatalogueItemsResponse; assetCategories: AssetCategoriesResponse; assetCategory: AssetCategoryResponse; assetClass: AssetClassResponse; assetClasses: AssetClassesResponse; + assetFromGs1Data: AssetParseResponse; assetLogReasons: AssetLogReasonsResponse; assetLogs: AssetLogsResponse; assetProperties: AssetPropertiesResponse; @@ -5630,12 +5635,6 @@ export type QueriesActivityLogsArgs = { }; -export type QueriesAssetByScannedStringArgs = { - inputText: Scalars['String']['input']; - storeId: Scalars['String']['input']; -}; - - export type QueriesAssetCatalogueItemArgs = { id: Scalars['String']['input']; }; @@ -5672,6 +5671,12 @@ export type QueriesAssetClassesArgs = { }; +export type QueriesAssetFromGs1DataArgs = { + gs1: Array; + storeId: Scalars['String']['input']; +}; + + export type QueriesAssetLogReasonsArgs = { filter?: InputMaybe; page?: InputMaybe; diff --git a/client/packages/common/src/utils/BarcodeScannerContext.tsx b/client/packages/common/src/utils/BarcodeScannerContext.tsx index 1bd23620ea..eb68b0237e 100644 --- a/client/packages/common/src/utils/BarcodeScannerContext.tsx +++ b/client/packages/common/src/utils/BarcodeScannerContext.tsx @@ -5,13 +5,14 @@ import { Capacitor } from '@capacitor/core'; import { GlobalStyles } from '@mui/material'; import { useNotification } from '../hooks/useNotification'; import { useTranslation } from '@common/intl'; -import { parseBarcode } from 'gs1-barcode-parser-mod'; +import { Gs1Barcode, parseBarcode } from 'gs1-barcode-parser-mod'; import { Formatter } from './formatters'; import { BarcodeScanner, ScannerType } from '@openmsupply-client/common'; const SCAN_TIMEOUT_IN_MS = 5000; export interface ScanResult { + gs1?: Gs1Barcode; batch?: string; content?: string; expiryDate?: string | null; diff --git a/server/graphql/asset/src/lib.rs b/server/graphql/asset/src/lib.rs index 6b972174f4..844738a34f 100644 --- a/server/graphql/asset/src/lib.rs +++ b/server/graphql/asset/src/lib.rs @@ -15,7 +15,7 @@ use service::auth::{Resource, ResourceAccessRequest}; use types::{ map_parse_error, AssetConnector, AssetFilterInput, AssetNode, AssetParseResponse, - AssetSortInput, AssetsResponse, ScannedDataParseError, + AssetSortInput, AssetsResponse, GS1DataElement, ScannedDataParseError, }; #[derive(Default, Clone)] @@ -61,11 +61,11 @@ impl AssetQueries { ))) } - pub async fn asset_by_scanned_string( + pub async fn asset_from_gs1_data( &self, ctx: &Context<'_>, store_id: String, - input_text: String, + gs1: Vec, ) -> Result { let user = validate_auth( ctx, @@ -78,9 +78,10 @@ impl AssetQueries { let service_provider = ctx.service_provider(); let service_context = service_provider.context(store_id.clone(), user.user_id)?; - let result = service_provider - .asset_service - .parse_scanned_data(&service_context, input_text); + let result = service_provider.asset_service.asset_from_gs1_data( + &service_context, + gs1.into_iter().map(GS1DataElement::to_domain).collect(), + ); match result { Ok(asset) => Ok(AssetParseResponse::Response(AssetNode::from_domain(asset))), diff --git a/server/graphql/asset/src/types/asset.rs b/server/graphql/asset/src/types/asset.rs index 6f26cced9d..35373c9e29 100644 --- a/server/graphql/asset/src/types/asset.rs +++ b/server/graphql/asset/src/types/asset.rs @@ -28,7 +28,7 @@ use repository::{ EqualFilter, }; use repository::{DateFilter, StringFilter}; -use service::asset::parse::ScannedDataParseError as ServiceScannedDataParseError; +use service::asset::parse::AssetFromGs1Error as ServiceScannedDataParseError; use service::{usize_to_u32, ListResult}; use super::{AssetLogNode, AssetLogStatusInput, EqualFilterStatusInput}; diff --git a/server/graphql/asset/src/types/gs1.rs b/server/graphql/asset/src/types/gs1.rs new file mode 100644 index 0000000000..e910accd5b --- /dev/null +++ b/server/graphql/asset/src/types/gs1.rs @@ -0,0 +1,17 @@ +use async_graphql::*; +use util::GS1DataElement as DomainGS1DataElement; + +#[derive(InputObject, Clone)] +pub struct GS1DataElement { + ai: String, + data: String, +} + +impl GS1DataElement { + pub fn to_domain(self) -> DomainGS1DataElement { + DomainGS1DataElement { + ai: self.ai.clone(), + data: self.data.clone(), + } + } +} diff --git a/server/graphql/asset/src/types/mod.rs b/server/graphql/asset/src/types/mod.rs index 2a2403b6e8..566d907166 100644 --- a/server/graphql/asset/src/types/mod.rs +++ b/server/graphql/asset/src/types/mod.rs @@ -4,3 +4,5 @@ pub mod asset; pub use asset::*; pub mod asset_property; pub use asset_property::*; +pub mod gs1; +pub use gs1::*; diff --git a/server/service/src/asset/mod.rs b/server/service/src/asset/mod.rs index 85b8315f3a..9657b5cbc5 100644 --- a/server/service/src/asset/mod.rs +++ b/server/service/src/asset/mod.rs @@ -13,13 +13,14 @@ use self::update::{update_asset, UpdateAsset, UpdateAssetError}; use super::{ListError, ListResult}; use crate::{service_provider::ServiceContext, SingleRecordError}; -use parse::ScannedDataParseError; +use parse::AssetFromGs1Error; use repository::asset_log_reason::{AssetLogReason, AssetLogReasonFilter, AssetLogReasonSort}; use repository::asset_property::AssetPropertyFilter; use repository::asset_property_row::AssetPropertyRow; use repository::assets::asset::{Asset, AssetFilter, AssetSort}; use repository::assets::asset_log::{AssetLog, AssetLogFilter, AssetLogSort}; use repository::{PaginationOption, StorageConnection}; +use util::GS1DataElement; pub mod delete; pub mod delete_log_reason; @@ -139,12 +140,12 @@ pub trait AssetServiceTrait: Sync + Send { get_asset_properties(connection, filter) } - fn parse_scanned_data( + fn asset_from_gs1_data( &self, ctx: &ServiceContext, - scanned_data: String, - ) -> Result { - parse::parse_from_scanned_data(ctx, scanned_data) + gs1_data: Vec, + ) -> Result { + parse::create_from_gs1_data(ctx, gs1_data) } } diff --git a/server/service/src/asset/parse.rs b/server/service/src/asset/parse.rs index 3a5c7d8fae..cd0ecf6377 100644 --- a/server/service/src/asset/parse.rs +++ b/server/service/src/asset/parse.rs @@ -4,12 +4,12 @@ use repository::{ asset_catalogue_item::{AssetCatalogueItemFilter, AssetCatalogueItemRepository}, EqualFilter, RepositoryError, StringFilter, }; -use util::{GS1ParseError, GS1}; +use util::{GS1DataElement, GS1}; use crate::service_provider::ServiceContext; #[derive(Debug)] -pub enum ScannedDataParseError { +pub enum AssetFromGs1Error { ParseError, MissingPartNumber, MissingSerialNumber, @@ -17,40 +17,27 @@ pub enum ScannedDataParseError { DatabaseError(RepositoryError), } -impl From for ScannedDataParseError { +impl From for AssetFromGs1Error { fn from(error: RepositoryError) -> Self { - ScannedDataParseError::DatabaseError(error) - } -} - -fn lookup_asset_by_id(ctx: &ServiceContext, id: &str) -> Result { - let repository = AssetRepository::new(&ctx.connection); - - let mut result = - repository.query_by_filter(AssetFilter::new().id(EqualFilter::equal_to(id)))?; - - if let Some(record) = result.pop() { - Ok(record) - } else { - Err(ScannedDataParseError::NotFound) + AssetFromGs1Error::DatabaseError(error) } } fn check_if_asset_already_exists( ctx: &ServiceContext, gs1: &GS1, -) -> Result, ScannedDataParseError> { +) -> Result, AssetFromGs1Error> { // Look up the item by the Serial Number & part number let serial_number = gs1 .serial_number() - .ok_or(ScannedDataParseError::MissingSerialNumber)?; + .ok_or(AssetFromGs1Error::MissingSerialNumber)?; log::info!("Looking up asset by serial number: {}", serial_number); let mut filter = AssetFilter::new().serial_number(StringFilter::equal_to(&serial_number)); let part_number = gs1 .part_number() - .ok_or(ScannedDataParseError::MissingPartNumber)?; + .ok_or(AssetFromGs1Error::MissingPartNumber)?; if let Some(asset_catalogue_id) = lookup_asset_catalogue_id_by_pqs_code(ctx, &part_number)? { filter = filter.catalogue_item_id(EqualFilter::equal_to(&asset_catalogue_id)); @@ -82,10 +69,7 @@ fn lookup_asset_catalogue_id_by_pqs_code( Ok(catalogue_item_id) } -fn create_draft_asset_from_gs1( - ctx: &ServiceContext, - gs1: GS1, -) -> Result { +fn create_draft_asset_from_gs1(ctx: &ServiceContext, gs1: GS1) -> Result { let mut asset = Asset::default(); asset.serial_number = gs1.serial_number(); @@ -97,9 +81,8 @@ fn create_draft_asset_from_gs1( gs1.serial_number().unwrap_or_default() )); - let (warranty_start, warranty_end) = gs1 - .warranty_dates() - .ok_or(ScannedDataParseError::ParseError)?; + let (warranty_start, warranty_end) = + gs1.warranty_dates().ok_or(AssetFromGs1Error::ParseError)?; asset.warranty_start = Some(warranty_start); asset.warranty_end = Some(warranty_end); @@ -113,23 +96,11 @@ fn create_draft_asset_from_gs1( Ok(asset) } -pub fn parse_from_scanned_data( +pub fn create_from_gs1_data( ctx: &ServiceContext, - scanned_data: String, -) -> Result { - log::info!("Parsing scanned data: {}", scanned_data); - - let result = GS1::parse(scanned_data.to_string()); - - let gs1 = match result { - Ok(gs1) => gs1, - Err(GS1ParseError::InvalidFormat) => { - log::info!( - "Scanned data is not GS1 data, it could be an asset ID from our own barcode" - ); - return lookup_asset_by_id(ctx, &scanned_data); - } - }; + gs1_data: Vec, +) -> Result { + let gs1 = GS1::from_data_elements(gs1_data); // Look up the item by the serial number & part number if let Some(asset) = check_if_asset_already_exists(ctx, &gs1)? { @@ -142,31 +113,12 @@ pub fn parse_from_scanned_data( #[cfg(test)] mod test { - use crate::{asset::parse::parse_from_scanned_data, service_provider::ServiceProvider}; + use crate::{asset::parse::create_from_gs1_data, service_provider::ServiceProvider}; use repository::{ mock::{mock_asset_a, mock_store_a, MockDataInserts}, test_db::setup_all, }; - - #[actix_rt::test] - async fn parse_asset_data_internal_id() { - let (_, _connection, connection_manager, _) = setup_all( - "parse_asset_data_internal_id", - MockDataInserts::none().assets().locations(), - ) - .await; - - let service_provider = ServiceProvider::new(connection_manager, "app_data"); - let ctx = service_provider - .context(mock_store_a().id, "".to_string()) - .unwrap(); - - // Check we can find an asset by ID if that's the input - let result = parse_from_scanned_data(&ctx, mock_asset_a().id.clone()); - let asset = result.unwrap(); - - assert_eq!(asset.id, mock_asset_a().id); - } + use util::GS1; #[actix_rt::test] async fn parse_asset_data_gs1_data() { @@ -185,7 +137,9 @@ mod test { let example_gs1 = "(01)00012345600012(11)241007(21)S12345678(241)E003/002(3121)82(3131)67(3111)63(8013)HBD 116(90)001(91)241007-310101(92){\"pqs\":\"https://apps.who.int/immunization_standards/vaccine_quality/pqs_catalogue/LinkPDF.aspx?UniqueID=3bf9439f-3316-49b4-845e-d50360f8280f&TipoDoc=DataSheet&ID=0\"}"; - let draft_asset = parse_from_scanned_data(&ctx, example_gs1.to_string()).unwrap(); + let gs1 = GS1::from_human_readable_string(example_gs1.to_string()).unwrap(); + + let draft_asset = create_from_gs1_data(&ctx, gs1.to_data_elements()).unwrap(); assert_eq!(draft_asset.id, ""); // Draft asset has an empty ID assert_eq!(draft_asset.serial_number, Some("S12345678".to_string())); @@ -224,7 +178,9 @@ mod test { mock_asset_a().serial_number.unwrap() ); // Note E003/002 has to match the catalogue item ID for mock_asset_a - let existing_asset = parse_from_scanned_data(&ctx, gs1_data).unwrap(); + let gs1 = GS1::from_human_readable_string(gs1_data).unwrap(); + + let existing_asset = create_from_gs1_data(&ctx, gs1.to_data_elements()).unwrap(); assert_eq!(existing_asset.id, mock_asset_a().id); } diff --git a/server/util/src/gs1.rs b/server/util/src/gs1.rs index a3fa4f17f0..a6cb3fa838 100644 --- a/server/util/src/gs1.rs +++ b/server/util/src/gs1.rs @@ -7,6 +7,12 @@ pub enum GS1ParseError { InvalidFormat, } +#[derive(Debug)] +pub struct GS1DataElement { + pub ai: String, + pub data: String, +} + #[derive(Debug)] pub struct GS1 { gs1: HashMap, @@ -19,7 +25,30 @@ impl GS1 { } } - pub fn parse(gs1_input: String) -> Result { + pub fn from_data_elements(data_elements: Vec) -> Self { + let mut gs1 = HashMap::new(); + + for data_element in data_elements { + gs1.insert(data_element.ai.clone(), data_element.data.clone()); + } + + Self { gs1 } + } + + pub fn to_data_elements(&self) -> Vec { + let mut data_elements = Vec::new(); + + for (ai, data) in &self.gs1 { + data_elements.push(GS1DataElement { + ai: ai.clone(), + data: data.clone(), + }); + } + + data_elements + } + + pub fn from_human_readable_string(gs1_input: String) -> Result { let gs1 = parse_gs1_string(gs1_input)?; Ok(Self { gs1 }) @@ -111,12 +140,41 @@ fn parse_gs1_string(gs1_input: String) -> Result, GS1Par mod test { use crate::{gs1::parse_gs1_string, GS1}; + #[test] + fn gs1_from_data_elements() { + let data_elements = vec![ + crate::GS1DataElement { + ai: "01".to_string(), + data: "123456".to_string(), + }, + crate::GS1DataElement { + ai: "21".to_string(), + data: "S12345678".to_string(), + }, + crate::GS1DataElement { + ai: "241".to_string(), + data: "E003/002".to_string(), + }, + ]; + + let gs1 = GS1::from_data_elements(data_elements); + + let gtin = gs1.get("01").unwrap(); + assert_eq!(gtin, "123456"); + + let serial = gs1.get("21").unwrap(); + assert_eq!(serial, "S12345678"); + + let part_number = gs1.get("241").unwrap(); + assert_eq!(part_number, "E003/002"); + } + #[test] fn test_parse_gs1_string() { // Test a simple GS1 string with just a GTIN let gs1_input = "(01)123456".to_string(); - let gs1 = GS1::parse(gs1_input).unwrap(); + let gs1 = GS1::from_human_readable_string(gs1_input).unwrap(); let gtin = gs1.get("01").unwrap();