Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2020 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 :
20 : /**
21 : * @file taler-merchant-httpd_private-post-products.c
22 : * @brief implementing POST /products request handling
23 : * @author Christian Grothoff
24 : */
25 : #include "platform.h"
26 : #include "taler-merchant-httpd_private-post-products.h"
27 : #include "taler-merchant-httpd_helper.h"
28 : #include <taler/taler_json_lib.h>
29 :
30 :
31 : /**
32 : * How often do we retry the simple INSERT database transaction?
33 : */
34 : #define MAX_RETRIES 3
35 :
36 :
37 : /**
38 : * Check if the two products are identical.
39 : *
40 : * @param p1 product to compare
41 : * @param p2 other product to compare
42 : * @return true if they are 'equal', false if not or of payto_uris is not an array
43 : */
44 : static bool
45 0 : products_equal (const struct TALER_MERCHANTDB_ProductDetails *p1,
46 : const struct TALER_MERCHANTDB_ProductDetails *p2)
47 : {
48 0 : return ( (0 == strcmp (p1->description,
49 0 : p2->description)) &&
50 0 : (1 == json_equal (p1->description_i18n,
51 0 : p2->description_i18n)) &&
52 0 : (0 == strcmp (p1->unit,
53 0 : p2->unit)) &&
54 : (GNUNET_OK ==
55 0 : TALER_amount_cmp_currency (&p1->price,
56 0 : &p2->price)) &&
57 0 : (0 == TALER_amount_cmp (&p1->price,
58 0 : &p2->price)) &&
59 0 : (1 == json_equal (p1->taxes,
60 0 : p2->taxes)) &&
61 0 : (p1->total_stock == p2->total_stock) &&
62 0 : (p1->total_sold == p2->total_sold) &&
63 0 : (p1->total_lost == p2->total_lost) &&
64 0 : (p1->minimum_age == p2->minimum_age) &&
65 0 : (0 == strcmp (p1->image,
66 0 : p2->image)) &&
67 0 : (1 == json_equal (p1->address,
68 0 : p2->address)) &&
69 0 : (GNUNET_TIME_timestamp_cmp (p1->next_restock,
70 : ==,
71 : p2->next_restock) ) );
72 : }
73 :
74 :
75 : MHD_RESULT
76 0 : TMH_private_post_products (const struct TMH_RequestHandler *rh,
77 : struct MHD_Connection *connection,
78 : struct TMH_HandlerContext *hc)
79 : {
80 0 : struct TMH_MerchantInstance *mi = hc->instance;
81 0 : struct TALER_MERCHANTDB_ProductDetails pd = { 0 };
82 : const char *product_id;
83 : int64_t total_stock;
84 : enum GNUNET_DB_QueryStatus qs;
85 : struct GNUNET_JSON_Specification spec[] = {
86 0 : GNUNET_JSON_spec_string ("product_id",
87 : &product_id),
88 0 : GNUNET_JSON_spec_string ("description",
89 : (const char **) &pd.description),
90 0 : GNUNET_JSON_spec_mark_optional (
91 : GNUNET_JSON_spec_json ("description_i18n",
92 : &pd.description_i18n),
93 : NULL),
94 0 : GNUNET_JSON_spec_string ("unit",
95 : (const char **) &pd.unit),
96 0 : TALER_JSON_spec_amount ("price",
97 : TMH_currency,
98 : &pd.price),
99 0 : GNUNET_JSON_spec_mark_optional (
100 : GNUNET_JSON_spec_string ("image",
101 : (const char **) &pd.image),
102 : NULL),
103 0 : GNUNET_JSON_spec_mark_optional (
104 : GNUNET_JSON_spec_json ("taxes",
105 : &pd.taxes),
106 : NULL),
107 0 : GNUNET_JSON_spec_int64 ("total_stock",
108 : &total_stock),
109 0 : GNUNET_JSON_spec_mark_optional (
110 : GNUNET_JSON_spec_json ("address",
111 : &pd.address),
112 : NULL),
113 0 : GNUNET_JSON_spec_mark_optional (
114 : GNUNET_JSON_spec_timestamp ("next_restock",
115 : &pd.next_restock),
116 : NULL),
117 0 : GNUNET_JSON_spec_mark_optional (
118 : GNUNET_JSON_spec_uint32 ("minimum_age",
119 : &pd.minimum_age),
120 : NULL),
121 0 : GNUNET_JSON_spec_end ()
122 : };
123 :
124 0 : GNUNET_assert (NULL != mi);
125 : {
126 : enum GNUNET_GenericReturnValue res;
127 :
128 0 : res = TALER_MHD_parse_json_data (connection,
129 0 : hc->request_body,
130 : spec);
131 0 : if (GNUNET_OK != res)
132 : {
133 0 : GNUNET_break_op (0);
134 : return (GNUNET_NO == res)
135 : ? MHD_YES
136 0 : : MHD_NO;
137 : }
138 : }
139 0 : if (total_stock < -1)
140 : {
141 0 : GNUNET_break_op (0);
142 0 : GNUNET_JSON_parse_free (spec);
143 0 : return TALER_MHD_reply_with_error (connection,
144 : MHD_HTTP_BAD_REQUEST,
145 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
146 : "total_stock");
147 : }
148 :
149 :
150 0 : if (-1 == total_stock)
151 0 : pd.total_stock = INT64_MAX;
152 : else
153 0 : pd.total_stock = (uint64_t) total_stock;
154 :
155 0 : if (NULL == pd.address)
156 0 : pd.address = json_object ();
157 0 : if (NULL == pd.description_i18n)
158 0 : pd.description_i18n = json_object ();
159 0 : if (NULL == pd.taxes)
160 0 : pd.taxes = json_array ();
161 :
162 : /* check taxes is well-formed */
163 0 : if (! TMH_taxes_array_valid (pd.taxes))
164 : {
165 0 : GNUNET_break_op (0);
166 0 : GNUNET_JSON_parse_free (spec);
167 0 : return TALER_MHD_reply_with_error (connection,
168 : MHD_HTTP_BAD_REQUEST,
169 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
170 : "taxes");
171 : }
172 :
173 0 : if (! TMH_location_object_valid (pd.address))
174 : {
175 0 : GNUNET_break_op (0);
176 0 : GNUNET_JSON_parse_free (spec);
177 0 : return TALER_MHD_reply_with_error (connection,
178 : MHD_HTTP_BAD_REQUEST,
179 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
180 : "address");
181 : }
182 :
183 0 : if (! TALER_JSON_check_i18n (pd.description_i18n))
184 : {
185 0 : GNUNET_break_op (0);
186 0 : GNUNET_JSON_parse_free (spec);
187 0 : return TALER_MHD_reply_with_error (connection,
188 : MHD_HTTP_BAD_REQUEST,
189 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
190 : "description_i18n");
191 : }
192 :
193 0 : if (NULL == pd.image)
194 0 : pd.image = "";
195 0 : if (! TMH_image_data_url_valid (pd.image))
196 : {
197 0 : GNUNET_break_op (0);
198 0 : GNUNET_JSON_parse_free (spec);
199 0 : return TALER_MHD_reply_with_error (connection,
200 : MHD_HTTP_BAD_REQUEST,
201 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
202 : "image");
203 : }
204 :
205 : /* finally, interact with DB until no serialization error */
206 0 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
207 : {
208 : /* Test if an product of this id is known */
209 : struct TALER_MERCHANTDB_ProductDetails epd;
210 :
211 0 : if (GNUNET_OK !=
212 0 : TMH_db->start (TMH_db->cls,
213 : "/post products"))
214 : {
215 0 : GNUNET_break (0);
216 0 : GNUNET_JSON_parse_free (spec);
217 0 : return TALER_MHD_reply_with_error (connection,
218 : MHD_HTTP_INTERNAL_SERVER_ERROR,
219 : TALER_EC_GENERIC_DB_START_FAILED,
220 : NULL);
221 : }
222 0 : qs = TMH_db->lookup_product (TMH_db->cls,
223 0 : mi->settings.id,
224 : product_id,
225 : &epd);
226 0 : switch (qs)
227 : {
228 0 : case GNUNET_DB_STATUS_HARD_ERROR:
229 : /* Clean up and fail hard */
230 0 : GNUNET_break (0);
231 0 : TMH_db->rollback (TMH_db->cls);
232 0 : GNUNET_JSON_parse_free (spec);
233 0 : return TALER_MHD_reply_with_error (connection,
234 : MHD_HTTP_INTERNAL_SERVER_ERROR,
235 : TALER_EC_GENERIC_DB_FETCH_FAILED,
236 : NULL);
237 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
238 : /* restart transaction */
239 0 : goto retry;
240 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
241 : /* Good, we can proceed! */
242 0 : break;
243 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
244 : /* idempotency check: is epd == pd? */
245 : {
246 : bool eq;
247 :
248 0 : eq = products_equal (&pd,
249 : &epd);
250 0 : TALER_MERCHANTDB_product_details_free (&epd);
251 0 : TMH_db->rollback (TMH_db->cls);
252 0 : GNUNET_JSON_parse_free (spec);
253 : return eq
254 0 : ? TALER_MHD_reply_static (connection,
255 : MHD_HTTP_NO_CONTENT,
256 : NULL,
257 : NULL,
258 : 0)
259 0 : : TALER_MHD_reply_with_error (connection,
260 : MHD_HTTP_CONFLICT,
261 : TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS,
262 : product_id);
263 : }
264 : } /* end switch (qs) */
265 :
266 0 : qs = TMH_db->insert_product (TMH_db->cls,
267 0 : mi->settings.id,
268 : product_id,
269 : &pd);
270 0 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
271 : {
272 0 : TMH_db->rollback (TMH_db->cls);
273 0 : break;
274 : }
275 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
276 : {
277 0 : qs = TMH_db->commit (TMH_db->cls);
278 0 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
279 0 : break;
280 : }
281 0 : retry:
282 0 : GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs);
283 0 : TMH_db->rollback (TMH_db->cls);
284 : } /* for RETRIES loop */
285 0 : GNUNET_JSON_parse_free (spec);
286 0 : if (qs < 0)
287 : {
288 0 : GNUNET_break (0);
289 0 : return TALER_MHD_reply_with_error (
290 : connection,
291 : MHD_HTTP_INTERNAL_SERVER_ERROR,
292 : (GNUNET_DB_STATUS_SOFT_ERROR == qs)
293 : ? TALER_EC_GENERIC_DB_SOFT_FAILURE
294 : : TALER_EC_GENERIC_DB_COMMIT_FAILED,
295 : NULL);
296 : }
297 0 : return TALER_MHD_reply_static (connection,
298 : MHD_HTTP_NO_CONTENT,
299 : NULL,
300 : NULL,
301 : 0);
302 : }
303 :
304 :
305 : /* end of taler-merchant-httpd_private-post-products.c */
|