diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index d79b99f60..c77049506 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -56,6 +56,7 @@ export 'src/model/product_stats.dart'; export 'src/model/product_tag.dart'; export 'src/model/recommended_daily_intake.dart'; export 'src/model/robotoff_question.dart'; +export 'src/model/robotoff_question_order.dart'; export 'src/model/search_result.dart'; export 'src/model/send_image.dart'; export 'src/model/sign_up_status.dart'; @@ -115,6 +116,8 @@ export 'src/prices/price_user.dart'; export 'src/prices/proof.dart'; export 'src/prices/proof_type.dart'; export 'src/prices/session.dart'; +export 'src/prices/update_price_parameters.dart'; +export 'src/prices/update_proof_parameters.dart'; export 'src/search/autocomplete_search_result.dart'; export 'src/search/autocomplete_single_result.dart'; export 'src/search/fuzziness.dart'; @@ -138,7 +141,6 @@ export 'src/utils/product_fields.dart'; export 'src/utils/product_helper.dart'; export 'src/utils/product_query_configurations.dart'; export 'src/utils/product_search_query_configuration.dart'; -export 'src/model/robotoff_question_order.dart'; export 'src/utils/server_type.dart'; export 'src/utils/suggestion_manager.dart'; export 'src/utils/tag_type.dart'; diff --git a/lib/src/open_prices_api_client.dart b/lib/src/open_prices_api_client.dart index d53c3e90a..53e931416 100644 --- a/lib/src/open_prices_api_client.dart +++ b/lib/src/open_prices_api_client.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; + import 'package:http/http.dart'; import 'package:http_parser/http_parser.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart'; +import 'prices/currency.dart'; import 'prices/maybe_error.dart'; import 'prices/price.dart'; import 'prices/proof.dart'; @@ -20,6 +23,8 @@ import 'prices/location_osm_type.dart'; import 'prices/price_product.dart'; import 'prices/proof_type.dart'; import 'prices/session.dart'; +import 'prices/update_price_parameters.dart'; +import 'prices/update_proof_parameters.dart'; import 'utils/http_helper.dart'; import 'utils/open_food_api_configuration.dart'; import 'utils/uri_helper.dart'; @@ -325,44 +330,27 @@ class OpenPricesAPIClient { path: '/api/v1/prices', uriHelper: uriHelper, ); - final StringBuffer body = StringBuffer(); - body.write('{'); - if (price.productCode != null) { - body.write('"product_code": "${price.productCode}",'); - } - if (price.productName != null) { - body.write('"product_name": "${price.productName}",'); - } - if (price.categoryTag != null) { - body.write('"category_tag": "${price.categoryTag}",'); - } - if (price.labelsTags != null) { - body.write('"labels_tags": "${price.labelsTags}",'); - } - if (price.originsTags != null) { - body.write('"origins_tags": "${price.originsTags}",'); - } - if (price.proofId != null) { - body.write('"proof_id": ${price.proofId},'); - } - if (price.pricePer != null) { - body.write('"price_per": "${price.pricePer!.offTag}",'); - } - if (price.priceWithoutDiscount != null) { - body.write('"price_without_discount": ${price.priceWithoutDiscount},'); - } - if (price.priceIsDiscounted != null) { - body.write('"price_is_discounted": ${price.priceIsDiscounted},'); - } - body.write('"price": ${price.price},'); - body.write('"currency": "${price.currency.name}",'); - body.write('"location_osm_id": ${price.locationOSMId},'); - body.write('"location_osm_type": "${price.locationOSMType.offTag}",'); - body.write('"date": "${GetParametersHelper.formatDate(price.date)}"'); - body.write('}'); + final Map body = { + if (price.productCode != null) 'product_code': price.productCode, + if (price.productName != null) 'product_name': price.productName, + if (price.categoryTag != null) 'category_tag': price.categoryTag, + if (price.labelsTags != null) 'labels_tags': price.labelsTags, + if (price.originsTags != null) 'origins_tags': price.originsTags, + if (price.proofId != null) 'proof_id': price.proofId, + if (price.pricePer != null) 'price_per': price.pricePer!.offTag, + if (price.priceWithoutDiscount != null) + 'price_without_discount': price.priceWithoutDiscount, + if (price.priceIsDiscounted != null) + 'price_is_discounted': price.priceIsDiscounted, + 'price': price.price, + 'currency': price.currency.name, + 'location_osm_id': price.locationOSMId, + 'location_osm_type': price.locationOSMType.offTag, + 'date': GetParametersHelper.formatDate(price.date), + }; final Response response = await HttpHelper().doPostJsonRequest( uri, - body.toString(), + jsonEncode(body), uriHelper: uriHelper, bearerToken: bearerToken, ); @@ -377,6 +365,39 @@ class OpenPricesAPIClient { return MaybeError.responseError(response); } + /// Updates a price. + /// + /// This endpoint requires authentication. + /// A user can update only owned prices. + static Future> updatePrice( + final int priceId, { + required final UpdatePriceParameters parameters, + final UriProductHelper uriHelper = uriHelperFoodProd, + required final String bearerToken, + }) async { + final Uri uri = getUri( + path: '/api/v1/prices/$priceId', + uriHelper: uriHelper, + ); + final Response response = await HttpHelper().doPatchRequest( + uri, + parameters.toJson(), + null, + uriHelper: uriHelper, + bearerToken: bearerToken, + addUserAgentParameters: false, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Price.fromJson(decodedResponse)); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + /// Deletes a price. /// A user can delete only owned prices. /// Returns true if successful. @@ -433,6 +454,10 @@ class OpenPricesAPIClient { required final ProofType proofType, required final Uri imageUri, required final MediaType mediaType, + final int? locationOSMId, + final LocationOSMType? locationOSMType, + final DateTime? date, + final Currency? currency, required final String bearerToken, final UriProductHelper uriHelper = uriHelperFoodProd, }) async { @@ -449,6 +474,11 @@ class OpenPricesAPIClient { request.fields.addAll( { 'type': proofType.offTag, + if (locationOSMId != null) 'location_osm_id': locationOSMId.toString(), + if (locationOSMType != null) + 'location_osm_type': locationOSMType.offTag, + if (date != null) 'date': GetParametersHelper.formatDate(date), + if (currency != null) 'currency': currency.name, }, ); final List fileBytes = await UriReader.instance!.readAsBytes(imageUri); @@ -506,8 +536,44 @@ class OpenPricesAPIClient { return MaybeError.responseError(response); } + /// Updates a proof. + /// + /// This endpoint requires authentication. + /// A user can update only owned proofs. + static Future> updateProof( + final int proofId, { + required final UpdateProofParameters parameters, + final UriProductHelper uriHelper = uriHelperFoodProd, + required final String bearerToken, + }) async { + final Uri uri = getUri( + path: '/api/v1/proofs/$proofId', + uriHelper: uriHelper, + ); + final Response response = await HttpHelper().doPatchRequest( + uri, + parameters.toJson(), + null, + uriHelper: uriHelper, + bearerToken: bearerToken, + addUserAgentParameters: false, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Proof.fromJson(decodedResponse)); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + /// Deletes a proof. - /// A user can delete only owned proofs. Can delete only proofs that are not associated with prices. A moderator can delete not owned proofs. + /// + /// A user can delete only owned proofs. + /// Can delete only proofs that are not associated with prices. + /// A moderator can delete not owned proofs. /// Returns true if successful. static Future> deleteProof({ required final int proofId, diff --git a/lib/src/prices/get_prices_parameters.dart b/lib/src/prices/get_prices_parameters.dart index 099c05cba..6a6816a2a 100644 --- a/lib/src/prices/get_prices_parameters.dart +++ b/lib/src/prices/get_prices_parameters.dart @@ -22,8 +22,10 @@ class GetPricesParameters extends GetParametersHelper { DateTime? dateGte; DateTime? dateLt; DateTime? dateLte; + int? proofId; String? owner; DateTime? createdGte; + DateTime? createdLte; @override Map getQueryParameters() { @@ -43,8 +45,10 @@ class GetPricesParameters extends GetParametersHelper { addNonNullDate(dateGte, 'date__gte', dayOnly: true); addNonNullDate(dateLt, 'date__lt', dayOnly: true); addNonNullDate(dateLte, 'date__lte', dayOnly: true); + addNonNullInt(proofId, 'proof_id'); addNonNullString(owner, 'owner'); addNonNullDate(createdGte, 'created__gte', dayOnly: false); + addNonNullDate(createdLte, 'created__lte', dayOnly: false); return result; } } diff --git a/lib/src/prices/get_proofs_parameters.dart b/lib/src/prices/get_proofs_parameters.dart index 2bfe6e1a8..d587efeef 100644 --- a/lib/src/prices/get_proofs_parameters.dart +++ b/lib/src/prices/get_proofs_parameters.dart @@ -1,6 +1,8 @@ import 'get_price_count_parameters_helper.dart'; import 'get_proofs_order.dart'; import 'proof_type.dart'; +import 'currency.dart'; +import 'location_osm_type.dart'; /// Parameters for the "get proofs" API query. /// @@ -9,6 +11,15 @@ class GetProofsParameters extends GetPriceCountParametersHelper { String? owner; ProofType? type; + int? locationOSMId; + LocationOSMType? locationOSMType; + int? locationId; + Currency? currency; + DateTime? date; + DateTime? dateGt; + DateTime? dateGte; + DateTime? dateLt; + DateTime? dateLte; /// Returns the parameters as a query parameter map. @override @@ -16,6 +27,15 @@ class GetProofsParameters super.getQueryParameters(); addNonNullString(owner, 'owner'); addNonNullString(type?.offTag, 'type'); + addNonNullInt(locationOSMId, 'location_osm_id'); + addNonNullString(locationOSMType?.offTag, 'location_osm_type'); + addNonNullInt(locationId, 'location_id'); + addNonNullString(currency?.name, 'currency'); + addNonNullDate(date, 'date', dayOnly: true); + addNonNullDate(dateGt, 'date__gt', dayOnly: true); + addNonNullDate(dateGte, 'date__gte', dayOnly: true); + addNonNullDate(dateLt, 'date__lt', dayOnly: true); + addNonNullDate(dateLte, 'date__lte', dayOnly: true); return result; } } diff --git a/lib/src/prices/price.dart b/lib/src/prices/price.dart index af37888dc..f5d3a7326 100644 --- a/lib/src/prices/price.dart +++ b/lib/src/prices/price.dart @@ -138,6 +138,10 @@ class Price extends JsonObject { @JsonKey(fromJson: JsonHelper.stringTimestampToDate) late DateTime created; + /// Latest update timestamp. Read-only. + @JsonKey(fromJson: JsonHelper.nullableStringTimestampToDate) + DateTime? updated; + Price(); factory Price.fromJson(Map json) => _$PriceFromJson(json); diff --git a/lib/src/prices/price.g.dart b/lib/src/prices/price.g.dart index 47563d76c..1f7da5309 100644 --- a/lib/src/prices/price.g.dart +++ b/lib/src/prices/price.g.dart @@ -37,7 +37,8 @@ Price _$PriceFromJson(Map json) => Price() ? null : PriceProduct.fromJson(json['product'] as Map) ..owner = json['owner'] as String - ..created = JsonHelper.stringTimestampToDate(json['created']); + ..created = JsonHelper.stringTimestampToDate(json['created']) + ..updated = JsonHelper.nullableStringTimestampToDate(json['updated']); Map _$PriceToJson(Price instance) => { 'product_code': instance.productCode, @@ -62,6 +63,7 @@ Map _$PriceToJson(Price instance) => { 'product': instance.product, 'owner': instance.owner, 'created': instance.created.toIso8601String(), + 'updated': instance.updated?.toIso8601String(), }; const _$PricePerEnumMap = { diff --git a/lib/src/prices/proof.dart b/lib/src/prices/proof.dart index 5ad0d6227..b2e102a53 100644 --- a/lib/src/prices/proof.dart +++ b/lib/src/prices/proof.dart @@ -1,6 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; -import 'proof_type.dart'; +import 'currency.dart'; +import 'location.dart'; +import 'location_osm_type.dart'; +import 'proof_type.dart'; import '../interface/json_object.dart'; import '../open_prices_api_client.dart'; import '../utils/json_helper.dart'; @@ -25,7 +28,7 @@ class Proof extends JsonObject { @JsonKey() late String mimetype; - /// Proof type. Read-only. + /// Proof type. @JsonKey() ProofType? type; @@ -33,6 +36,36 @@ class Proof extends JsonObject { @JsonKey(name: 'price_count') late int priceCount; + /// ID of the location in OpenStreetMap. + /// + /// The store where the product was bought. + @JsonKey(name: 'location_osm_id') + int? locationOSMId; + + /// Type of the OpenStreetMap location object. + /// + /// Stores can be represented as nodes, ways or relations in OpenStreetMap. + /// It is necessary to be able to fetch the correct information about the + /// store using the ID. + @JsonKey(name: 'location_osm_type') + LocationOSMType? locationOSMType; + + /// Location ID. + @JsonKey(name: 'location_id') + int? locationId; + + /// Date when the product was bought. + @JsonKey(fromJson: JsonHelper.nullableStringTimestampToDate) + DateTime? date; + + /// Currency of the price. + /// + /// The currency must be a valid currency code. + /// See https://en.wikipedia.org/wiki/ISO_4217 for a list of valid currency + /// codes. + @JsonKey() + Currency? currency; + /// Owner. Read-only. @JsonKey() late String owner; @@ -41,6 +74,14 @@ class Proof extends JsonObject { @JsonKey(fromJson: JsonHelper.stringTimestampToDate) late DateTime created; + /// Latest update timestamp. Read-only. + @JsonKey(fromJson: JsonHelper.nullableStringTimestampToDate) + DateTime? updated; + + /// Location. Read-only. + @JsonKey() + Location? location; + Proof(); factory Proof.fromJson(Map json) => _$ProofFromJson(json); diff --git a/lib/src/prices/proof.g.dart b/lib/src/prices/proof.g.dart index 4ef52c90a..614a9363b 100644 --- a/lib/src/prices/proof.g.dart +++ b/lib/src/prices/proof.g.dart @@ -12,8 +12,18 @@ Proof _$ProofFromJson(Map json) => Proof() ..mimetype = json['mimetype'] as String ..type = $enumDecodeNullable(_$ProofTypeEnumMap, json['type']) ..priceCount = (json['price_count'] as num).toInt() + ..locationOSMId = (json['location_osm_id'] as num?)?.toInt() + ..locationOSMType = + $enumDecodeNullable(_$LocationOSMTypeEnumMap, json['location_osm_type']) + ..locationId = (json['location_id'] as num?)?.toInt() + ..date = JsonHelper.nullableStringTimestampToDate(json['date']) + ..currency = $enumDecodeNullable(_$CurrencyEnumMap, json['currency']) ..owner = json['owner'] as String - ..created = JsonHelper.stringTimestampToDate(json['created']); + ..created = JsonHelper.stringTimestampToDate(json['created']) + ..updated = JsonHelper.nullableStringTimestampToDate(json['updated']) + ..location = json['location'] == null + ? null + : Location.fromJson(json['location'] as Map); Map _$ProofToJson(Proof instance) => { 'id': instance.id, @@ -21,8 +31,15 @@ Map _$ProofToJson(Proof instance) => { 'mimetype': instance.mimetype, 'type': _$ProofTypeEnumMap[instance.type], 'price_count': instance.priceCount, + 'location_osm_id': instance.locationOSMId, + 'location_osm_type': _$LocationOSMTypeEnumMap[instance.locationOSMType], + 'location_id': instance.locationId, + 'date': instance.date?.toIso8601String(), + 'currency': _$CurrencyEnumMap[instance.currency], 'owner': instance.owner, 'created': instance.created.toIso8601String(), + 'updated': instance.updated?.toIso8601String(), + 'location': instance.location, }; const _$ProofTypeEnumMap = { @@ -30,3 +47,317 @@ const _$ProofTypeEnumMap = { ProofType.receipt: 'RECEIPT', ProofType.gdprRequest: 'GDPR_REQUEST', }; + +const _$LocationOSMTypeEnumMap = { + LocationOSMType.node: 'NODE', + LocationOSMType.way: 'WAY', + LocationOSMType.relation: 'RELATION', +}; + +const _$CurrencyEnumMap = { + Currency.ADP: 'ADP', + Currency.AED: 'AED', + Currency.AFA: 'AFA', + Currency.AFN: 'AFN', + Currency.ALK: 'ALK', + Currency.ALL: 'ALL', + Currency.AMD: 'AMD', + Currency.ANG: 'ANG', + Currency.AOA: 'AOA', + Currency.AOK: 'AOK', + Currency.AON: 'AON', + Currency.AOR: 'AOR', + Currency.ARA: 'ARA', + Currency.ARL: 'ARL', + Currency.ARM: 'ARM', + Currency.ARP: 'ARP', + Currency.ARS: 'ARS', + Currency.ATS: 'ATS', + Currency.AUD: 'AUD', + Currency.AWG: 'AWG', + Currency.AZM: 'AZM', + Currency.AZN: 'AZN', + Currency.BAD: 'BAD', + Currency.BAM: 'BAM', + Currency.BAN: 'BAN', + Currency.BBD: 'BBD', + Currency.BDT: 'BDT', + Currency.BEC: 'BEC', + Currency.BEF: 'BEF', + Currency.BEL: 'BEL', + Currency.BGL: 'BGL', + Currency.BGM: 'BGM', + Currency.BGN: 'BGN', + Currency.BGO: 'BGO', + Currency.BHD: 'BHD', + Currency.BIF: 'BIF', + Currency.BMD: 'BMD', + Currency.BND: 'BND', + Currency.BOB: 'BOB', + Currency.BOL: 'BOL', + Currency.BOP: 'BOP', + Currency.BOV: 'BOV', + Currency.BRB: 'BRB', + Currency.BRC: 'BRC', + Currency.BRE: 'BRE', + Currency.BRL: 'BRL', + Currency.BRN: 'BRN', + Currency.BRR: 'BRR', + Currency.BRZ: 'BRZ', + Currency.BSD: 'BSD', + Currency.BTN: 'BTN', + Currency.BUK: 'BUK', + Currency.BWP: 'BWP', + Currency.BYB: 'BYB', + Currency.BYN: 'BYN', + Currency.BYR: 'BYR', + Currency.BZD: 'BZD', + Currency.CAD: 'CAD', + Currency.CDF: 'CDF', + Currency.CHE: 'CHE', + Currency.CHF: 'CHF', + Currency.CHW: 'CHW', + Currency.CLE: 'CLE', + Currency.CLF: 'CLF', + Currency.CLP: 'CLP', + Currency.CNH: 'CNH', + Currency.CNX: 'CNX', + Currency.CNY: 'CNY', + Currency.COP: 'COP', + Currency.COU: 'COU', + Currency.CRC: 'CRC', + Currency.CSD: 'CSD', + Currency.CSK: 'CSK', + Currency.CUC: 'CUC', + Currency.CUP: 'CUP', + Currency.CVE: 'CVE', + Currency.CYP: 'CYP', + Currency.CZK: 'CZK', + Currency.DDM: 'DDM', + Currency.DEM: 'DEM', + Currency.DJF: 'DJF', + Currency.DKK: 'DKK', + Currency.DOP: 'DOP', + Currency.DZD: 'DZD', + Currency.ECS: 'ECS', + Currency.ECV: 'ECV', + Currency.EEK: 'EEK', + Currency.EGP: 'EGP', + Currency.ERN: 'ERN', + Currency.ESA: 'ESA', + Currency.ESB: 'ESB', + Currency.ESP: 'ESP', + Currency.ETB: 'ETB', + Currency.EUR: 'EUR', + Currency.FIM: 'FIM', + Currency.FJD: 'FJD', + Currency.FKP: 'FKP', + Currency.FRF: 'FRF', + Currency.GBP: 'GBP', + Currency.GEK: 'GEK', + Currency.GEL: 'GEL', + Currency.GHC: 'GHC', + Currency.GHS: 'GHS', + Currency.GIP: 'GIP', + Currency.GMD: 'GMD', + Currency.GNF: 'GNF', + Currency.GNS: 'GNS', + Currency.GQE: 'GQE', + Currency.GRD: 'GRD', + Currency.GTQ: 'GTQ', + Currency.GWE: 'GWE', + Currency.GWP: 'GWP', + Currency.GYD: 'GYD', + Currency.HKD: 'HKD', + Currency.HNL: 'HNL', + Currency.HRD: 'HRD', + Currency.HRK: 'HRK', + Currency.HTG: 'HTG', + Currency.HUF: 'HUF', + Currency.IDR: 'IDR', + Currency.IEP: 'IEP', + Currency.ILP: 'ILP', + Currency.ILR: 'ILR', + Currency.ILS: 'ILS', + Currency.INR: 'INR', + Currency.IQD: 'IQD', + Currency.IRR: 'IRR', + Currency.ISJ: 'ISJ', + Currency.ISK: 'ISK', + Currency.ITL: 'ITL', + Currency.JMD: 'JMD', + Currency.JOD: 'JOD', + Currency.JPY: 'JPY', + Currency.KES: 'KES', + Currency.KGS: 'KGS', + Currency.KHR: 'KHR', + Currency.KMF: 'KMF', + Currency.KPW: 'KPW', + Currency.KRH: 'KRH', + Currency.KRO: 'KRO', + Currency.KRW: 'KRW', + Currency.KWD: 'KWD', + Currency.KYD: 'KYD', + Currency.KZT: 'KZT', + Currency.LAK: 'LAK', + Currency.LBP: 'LBP', + Currency.LKR: 'LKR', + Currency.LRD: 'LRD', + Currency.LSL: 'LSL', + Currency.LTL: 'LTL', + Currency.LTT: 'LTT', + Currency.LUC: 'LUC', + Currency.LUF: 'LUF', + Currency.LUL: 'LUL', + Currency.LVL: 'LVL', + Currency.LVR: 'LVR', + Currency.LYD: 'LYD', + Currency.MAD: 'MAD', + Currency.MAF: 'MAF', + Currency.MCF: 'MCF', + Currency.MDC: 'MDC', + Currency.MDL: 'MDL', + Currency.MGA: 'MGA', + Currency.MGF: 'MGF', + Currency.MKD: 'MKD', + Currency.MKN: 'MKN', + Currency.MLF: 'MLF', + Currency.MMK: 'MMK', + Currency.MNT: 'MNT', + Currency.MOP: 'MOP', + Currency.MRO: 'MRO', + Currency.MRU: 'MRU', + Currency.MTL: 'MTL', + Currency.MTP: 'MTP', + Currency.MUR: 'MUR', + Currency.MVP: 'MVP', + Currency.MVR: 'MVR', + Currency.MWK: 'MWK', + Currency.MXN: 'MXN', + Currency.MXP: 'MXP', + Currency.MXV: 'MXV', + Currency.MYR: 'MYR', + Currency.MZE: 'MZE', + Currency.MZM: 'MZM', + Currency.MZN: 'MZN', + Currency.NAD: 'NAD', + Currency.NGN: 'NGN', + Currency.NIC: 'NIC', + Currency.NIO: 'NIO', + Currency.NLG: 'NLG', + Currency.NOK: 'NOK', + Currency.NPR: 'NPR', + Currency.NZD: 'NZD', + Currency.OMR: 'OMR', + Currency.PAB: 'PAB', + Currency.PEI: 'PEI', + Currency.PEN: 'PEN', + Currency.PES: 'PES', + Currency.PGK: 'PGK', + Currency.PHP: 'PHP', + Currency.PKR: 'PKR', + Currency.PLN: 'PLN', + Currency.PLZ: 'PLZ', + Currency.PTE: 'PTE', + Currency.PYG: 'PYG', + Currency.QAR: 'QAR', + Currency.RHD: 'RHD', + Currency.ROL: 'ROL', + Currency.RON: 'RON', + Currency.RSD: 'RSD', + Currency.RUB: 'RUB', + Currency.RUR: 'RUR', + Currency.RWF: 'RWF', + Currency.SAR: 'SAR', + Currency.SBD: 'SBD', + Currency.SCR: 'SCR', + Currency.SDD: 'SDD', + Currency.SDG: 'SDG', + Currency.SDP: 'SDP', + Currency.SEK: 'SEK', + Currency.SGD: 'SGD', + Currency.SHP: 'SHP', + Currency.SIT: 'SIT', + Currency.SKK: 'SKK', + Currency.SLE: 'SLE', + Currency.SLL: 'SLL', + Currency.SOS: 'SOS', + Currency.SRD: 'SRD', + Currency.SRG: 'SRG', + Currency.SSP: 'SSP', + Currency.STD: 'STD', + Currency.STN: 'STN', + Currency.SUR: 'SUR', + Currency.SVC: 'SVC', + Currency.SYP: 'SYP', + Currency.SZL: 'SZL', + Currency.THB: 'THB', + Currency.TJR: 'TJR', + Currency.TJS: 'TJS', + Currency.TMM: 'TMM', + Currency.TMT: 'TMT', + Currency.TND: 'TND', + Currency.TOP: 'TOP', + Currency.TPE: 'TPE', + Currency.TRL: 'TRL', + Currency.TRY: 'TRY', + Currency.TTD: 'TTD', + Currency.TWD: 'TWD', + Currency.TZS: 'TZS', + Currency.UAH: 'UAH', + Currency.UAK: 'UAK', + Currency.UGS: 'UGS', + Currency.UGX: 'UGX', + Currency.USD: 'USD', + Currency.USN: 'USN', + Currency.USS: 'USS', + Currency.UYI: 'UYI', + Currency.UYP: 'UYP', + Currency.UYU: 'UYU', + Currency.UYW: 'UYW', + Currency.UZS: 'UZS', + Currency.VEB: 'VEB', + Currency.VED: 'VED', + Currency.VEF: 'VEF', + Currency.VES: 'VES', + Currency.VND: 'VND', + Currency.VNN: 'VNN', + Currency.VUV: 'VUV', + Currency.WST: 'WST', + Currency.XAF: 'XAF', + Currency.XAG: 'XAG', + Currency.XAU: 'XAU', + Currency.XBA: 'XBA', + Currency.XBB: 'XBB', + Currency.XBC: 'XBC', + Currency.XBD: 'XBD', + Currency.XCD: 'XCD', + Currency.XDR: 'XDR', + Currency.XEU: 'XEU', + Currency.XFO: 'XFO', + Currency.XFU: 'XFU', + Currency.XOF: 'XOF', + Currency.XPD: 'XPD', + Currency.XPF: 'XPF', + Currency.XPT: 'XPT', + Currency.XRE: 'XRE', + Currency.XSU: 'XSU', + Currency.XTS: 'XTS', + Currency.XUA: 'XUA', + Currency.XXX: 'XXX', + Currency.YDD: 'YDD', + Currency.YER: 'YER', + Currency.YUD: 'YUD', + Currency.YUM: 'YUM', + Currency.YUN: 'YUN', + Currency.YUR: 'YUR', + Currency.ZAL: 'ZAL', + Currency.ZAR: 'ZAR', + Currency.ZMK: 'ZMK', + Currency.ZMW: 'ZMW', + Currency.ZRN: 'ZRN', + Currency.ZRZ: 'ZRZ', + Currency.ZWD: 'ZWD', + Currency.ZWL: 'ZWL', + Currency.ZWR: 'ZWR', +}; diff --git a/lib/src/prices/update_price_parameters.dart b/lib/src/prices/update_price_parameters.dart new file mode 100644 index 000000000..c83ec9b66 --- /dev/null +++ b/lib/src/prices/update_price_parameters.dart @@ -0,0 +1,38 @@ +import '../interface/json_object.dart'; +import 'currency.dart'; +import 'get_parameters_helper.dart'; +import 'price_per.dart'; + +/// Parameters for the "update price" API query. +/// +/// cf. https://prices.openfoodfacts.org/api/docs +class UpdatePriceParameters extends JsonObject { + /// Price of the product, without its currency, taxes included. + num? price; + + /// True if the price is discounted. + bool? priceIsDiscounted; + + /// Price of the product, without discount, taxes included. + num? priceWithoutDiscount; + + /// Price per unit, kilogram, ..? + PricePer? pricePer; + + /// Currency of the price. + Currency? currency; + + /// Date when the product was bought. + DateTime? date; + + @override + Map toJson() => { + if (pricePer != null) 'price_per': pricePer!.offTag, + if (priceWithoutDiscount != null) + 'price_without_discount': priceWithoutDiscount, + if (priceIsDiscounted != null) 'price_is_discounted': priceIsDiscounted, + if (price != null) 'price': price, + if (currency != null) 'currency': currency!.name, + if (date != null) 'date': GetParametersHelper.formatDate(date!), + }; +} diff --git a/lib/src/prices/update_proof_parameters.dart b/lib/src/prices/update_proof_parameters.dart new file mode 100644 index 000000000..db140facc --- /dev/null +++ b/lib/src/prices/update_proof_parameters.dart @@ -0,0 +1,25 @@ +import '../interface/json_object.dart'; +import 'currency.dart'; +import 'get_parameters_helper.dart'; +import 'proof_type.dart'; + +/// Parameters for the "update proof" API query. +/// +/// cf. https://prices.openfoodfacts.org/api/docs +class UpdateProofParameters extends JsonObject { + /// Proof type. + ProofType? type; + + /// Date when the product was bought. + DateTime? date; + + /// Currency of the price. + Currency? currency; + + @override + Map toJson() => { + if (type != null) 'type': type!.offTag, + if (date != null) 'date': GetParametersHelper.formatDate(date!), + if (currency != null) 'currency': currency!.name, + }; +} diff --git a/lib/src/utils/http_helper.dart b/lib/src/utils/http_helper.dart index 35cfe691b..4a46e302d 100644 --- a/lib/src/utils/http_helper.dart +++ b/lib/src/utils/http_helper.dart @@ -155,6 +155,8 @@ class HttpHelper { final Map body, final User? user, { required final UriHelper uriHelper, + final String? bearerToken, + final bool addUserAgentParameters = true, }) async => http.patch( uri, @@ -162,8 +164,13 @@ class HttpHelper { user: user, uriHelper: uriHelper, addCredentialsToHeader: false, + bearerToken: bearerToken, + ), + body: jsonEncode( + addUserAgentParameters + ? HttpHelper.addUserAgentParameters(body) + : body, ), - body: jsonEncode(addUserAgentParameters(body)), ); /// Send a multipart post request to the specified uri. diff --git a/test/api_prices_test.dart b/test/api_prices_test.dart index 79a11ed07..cedd2261e 100644 --- a/test/api_prices_test.dart +++ b/test/api_prices_test.dart @@ -7,12 +7,12 @@ import 'test_constants.dart'; void main() { OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; - const UriProductHelper uriHelper = uriHelperFoodTest; - const User user = TestConstants.TEST_USER; const String invalidBearerToken = 'invalid bearer token'; const int HTTP_OK = 200; group('$OpenPricesAPIClient default', () { + const UriProductHelper uriHelper = uriHelperFoodProd; + test('getStatus', () async { final MaybeError status = await OpenPricesAPIClient.getStatus( uriHelper: uriHelper, @@ -23,6 +23,9 @@ void main() { }); group('$OpenPricesAPIClient Auth', () { + const UriProductHelper uriHelper = uriHelperFoodTest; + const User user = TestConstants.TEST_USER; + test('unknown user', () async { final MaybeError status = await OpenPricesAPIClient.getAuthenticationToken( @@ -79,15 +82,26 @@ void main() { }); group('$OpenPricesAPIClient Prices', () { + const UriProductHelper uriHelper = uriHelperFoodTest; + const User user = TestConstants.TEST_USER; + test('create', () async { final Price initialPrice = Price() ..productCode = '3560071492755' ..price = 3.99 + ..pricePer = PricePer.unit ..currency = Currency.EUR ..locationOSMId = 4966187139 ..locationOSMType = LocationOSMType.node ..date = DateTime(2024, 1, 18); - //,"proof_id":1663,"product_id":null,"location_id":null + + final UpdatePriceParameters parameters = UpdatePriceParameters() + ..currency = Currency.USD + ..date = DateTime(2024, 1, 19) + ..pricePer = PricePer.kilogram + ..price = 12 + ..priceWithoutDiscount = 13 + ..priceIsDiscounted = true; String bearerToken = invalidBearerToken; @@ -121,6 +135,12 @@ void main() { expect(addedPrice.isError, isFalse); expect(addedPrice.value.productCode, initialPrice.productCode); expect(addedPrice.value.price, initialPrice.price); + expect(addedPrice.value.priceWithoutDiscount, + initialPrice.priceWithoutDiscount); + expect(addedPrice.value.priceIsDiscounted, + initialPrice.priceIsDiscounted ?? false); + // TODO(monsieurtanuki): uncomment when fixed server-side + //expect(addedPrice.value.pricePer, initialPrice.pricePer); expect(addedPrice.value.currency, initialPrice.currency); expect(addedPrice.value.locationOSMId, initialPrice.locationOSMId); expect(addedPrice.value.locationOSMType, initialPrice.locationOSMType); @@ -129,6 +149,22 @@ void main() { final int priceId = addedPrice.value.id; + // successful price update + addedPrice = await OpenPricesAPIClient.updatePrice( + priceId, + parameters: parameters, + bearerToken: bearerToken, + uriHelper: uriHelper, + ); + expect(addedPrice.isError, isFalse); + expect(addedPrice.value.price, parameters.price); + expect(addedPrice.value.priceWithoutDiscount, + parameters.priceWithoutDiscount); + expect(addedPrice.value.priceIsDiscounted, parameters.priceIsDiscounted); + expect(addedPrice.value.pricePer, parameters.pricePer); + expect(addedPrice.value.currency, parameters.currency); + expect(addedPrice.value.date, parameters.date); + // delete price first time: success MaybeError deleted = await OpenPricesAPIClient.deletePrice( priceId: priceId, @@ -159,6 +195,7 @@ void main() { }); test('get prices', () async { + const UriProductHelper uriHelper = uriHelperFoodProd; const int pageNumber = 1; const int pageSize = 20; @@ -284,6 +321,8 @@ void main() { }); group('$OpenPricesAPIClient Locations', () { + const UriProductHelper uriHelper = uriHelperFoodProd; + test('get existing location', () async { const int locationId = 1; final MaybeError maybeLocation = @@ -436,6 +475,8 @@ void main() { }); group('$OpenPricesAPIClient Products', () { + const UriProductHelper uriHelper = uriHelperFoodProd; + test('get existing product by ID', () async { const int productId = 1; final MaybeError maybePriceProduct = @@ -493,6 +534,9 @@ void main() { }); group('$OpenPricesAPIClient Proofs', () { + const UriProductHelper uriHelper = uriHelperFoodTest; + const User user = TestConstants.TEST_USER; + test('image file media type', () async { final Map expectedMediaTypes = { 'toto.jpeg': MediaType('image', 'jpeg'), @@ -621,7 +665,15 @@ void main() { }); test('upload', () async { - final ProofType initialProofType = ProofType.receipt; + final ProofType uploadProofType = ProofType.receipt; + const Currency uploadCurrency = Currency.EUR; + final DateTime uploadDate = DateTime(2024, 1, 1); + + final UpdateProofParameters parameters = UpdateProofParameters() + ..type = ProofType.priceTag + ..currency = Currency.USD + ..date = DateTime(2024, 1, 2); + // TODO(monsieurtanuki): more relevant image if possible final Uri initialImageUri = Uri.file('test/test_assets/ingredients_en.jpg'); @@ -632,7 +684,7 @@ void main() { // failing proof upload with invalid token MaybeError uploadProof = await OpenPricesAPIClient.uploadProof( - proofType: initialProofType, + proofType: uploadProofType, imageUri: initialImageUri, mediaType: initialMediaType, bearerToken: bearerToken, @@ -655,21 +707,37 @@ void main() { // successful proof upload with valid token uploadProof = await OpenPricesAPIClient.uploadProof( - proofType: initialProofType, + proofType: uploadProofType, imageUri: initialImageUri, mediaType: initialMediaType, + currency: uploadCurrency, + date: uploadDate, bearerToken: bearerToken, uriHelper: uriHelper, ); expect(uploadProof.isError, isFalse); - expect(uploadProof.value.type, initialProofType); + expect(uploadProof.value.type, uploadProofType); expect(uploadProof.value.owner, user.userId); expect(uploadProof.value.id, isNotNull); expect(uploadProof.value.priceCount, 0); expect(uploadProof.value.mimetype, initialMediaType.toString()); + expect(uploadProof.value.currency, uploadCurrency); + expect(uploadProof.value.date, uploadDate); final int proofId = uploadProof.value.id; + // successful proof update + uploadProof = await OpenPricesAPIClient.updateProof( + proofId, + parameters: parameters, + bearerToken: bearerToken, + uriHelper: uriHelper, + ); + expect(uploadProof.isError, isFalse); + expect(uploadProof.value.type, parameters.type); + expect(uploadProof.value.currency, parameters.currency); + expect(uploadProof.value.date, parameters.date); + // delete proof first time: success MaybeError deleted = await OpenPricesAPIClient.deleteProof( proofId: proofId, @@ -701,6 +769,8 @@ void main() { }); group('$OpenPricesAPIClient Users', () { + const UriProductHelper uriHelper = uriHelperFoodProd; + test('get users', () async { const int pageNumber = 1; const int pageSize = 20; @@ -722,7 +792,7 @@ void main() { try { maybeResults = await OpenPricesAPIClient.getUsers( parameters, - uriHelper: uriHelperFoodProd, + uriHelper: uriHelper, ); } catch (e) { if (e.toString().contains(TestConstants.badGatewayError)) {