Current section

7 Versions

Jump to

Compare versions

5 files changed
+286 additions
-226 deletions
  @@ -1,5 +1,11 @@
1 1 # ExCatalog
2 2
3 + [![Coverage Status](https://coveralls.io/repos/github/mithereal/ex_catalog/badge.svg?branch=main)](https://coveralls.io/github/mithereal/ex_catalog?branch=main)
4 + ![CircleCI](https://img.shields.io/circleci/build/github/mithereal/ex_catalog)
5 + [![Version](https://img.shields.io/hexpm/v/ex_catalog.svg?style=flat-square)](https://hex.pm/packages/ex_catalog)
6 + ![GitHub](https://img.shields.io/github/license/mithereal/ex_catalog)
7 + ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/mithereal/ex_catalog/main)
8 +
3 9 ** A General E-commerce Catalog System **
4 10
5 11 Think of this like an old school paper mail in catalog, we dont need all the fluff (extra db tables etc) as in in most ecommerce implementations, this is just a catalog, decoupled from inventory management with some options such as csv and pdf export, integer or binary primary key.
  @@ -1,7 +1,9 @@
1 - {<<"app">>,<<"ex_catalog">>}.
2 - {<<"build_tools">>,[<<"mix">>]}.
1 + {<<"links">>,[{<<"GitHub">>,<<"https://github.com/mithereal/ex_catalog">>}]}.
2 + {<<"name">>,<<"ex_catalog">>}.
3 + {<<"version">>,<<"1.5.6">>}.
3 4 {<<"description">>,<<"E-commerce Catalog for Elixir.">>}.
4 5 {<<"elixir">>,<<"~> 1.13">>}.
6 + {<<"app">>,<<"ex_catalog">>}.
5 7 {<<"files">>,
6 8 [<<"lib">>,<<"lib/ex_catalog.ex">>,<<"lib/ex_catalog">>,
7 9 <<"lib/ex_catalog/tasks">>,<<"lib/ex_catalog/tasks/currency_reload.ex">>,
  @@ -32,57 +34,55 @@
32 34 <<"lib/ex_catalog/template/category.ex">>,
33 35 <<"lib/ex_catalog/template/product.ex">>,<<"mix.exs">>,<<"README.md">>]}.
34 36 {<<"licenses">>,[<<"Apache-2.0">>]}.
35 - {<<"links">>,[{<<"GitHub">>,<<"https://github.com/mithereal/ex_catalog">>}]}.
36 - {<<"name">>,<<"ex_catalog">>}.
37 37 {<<"requirements">>,
38 - [[{<<"app">>,<<"postgrex">>},
39 - {<<"name">>,<<"postgrex">>},
38 + [[{<<"name">>,<<"postgrex">>},
39 + {<<"app">>,<<"postgrex">>},
40 40 {<<"optional">>,false},
41 - {<<"repository">>,<<"hexpm">>},
42 - {<<"requirement">>,<<">= 0.0.0">>}],
43 - [{<<"app">>,<<"ecto">>},
44 - {<<"name">>,<<"ecto">>},
41 + {<<"requirement">>,<<">= 0.0.0">>},
42 + {<<"repository">>,<<"hexpm">>}],
43 + [{<<"name">>,<<"ecto">>},
44 + {<<"app">>,<<"ecto">>},
45 45 {<<"optional">>,false},
46 - {<<"repository">>,<<"hexpm">>},
47 - {<<"requirement">>,<<"~> 3.5">>}],
48 - [{<<"app">>,<<"ecto_sql">>},
49 - {<<"name">>,<<"ecto_sql">>},
46 + {<<"requirement">>,<<"~> 3.5">>},
47 + {<<"repository">>,<<"hexpm">>}],
48 + [{<<"name">>,<<"ecto_sql">>},
49 + {<<"app">>,<<"ecto_sql">>},
50 50 {<<"optional">>,false},
51 - {<<"repository">>,<<"hexpm">>},
52 - {<<"requirement">>,<<"~> 3.5">>}],
53 - [{<<"app">>,<<"ecto_soft_delete">>},
54 - {<<"name">>,<<"ecto_soft_delete">>},
51 + {<<"requirement">>,<<"~> 3.5">>},
52 + {<<"repository">>,<<"hexpm">>}],
53 + [{<<"name">>,<<"ecto_soft_delete">>},
54 + {<<"app">>,<<"ecto_soft_delete">>},
55 55 {<<"optional">>,false},
56 - {<<"repository">>,<<"hexpm">>},
57 - {<<"requirement">>,<<">= 0.0.0">>}],
58 - [{<<"app">>,<<"ex_money">>},
59 - {<<"name">>,<<"ex_money">>},
56 + {<<"requirement">>,<<">= 0.0.0">>},
57 + {<<"repository">>,<<"hexpm">>}],
58 + [{<<"name">>,<<"ex_money">>},
59 + {<<"app">>,<<"ex_money">>},
60 60 {<<"optional">>,false},
61 - {<<"repository">>,<<"hexpm">>},
62 - {<<"requirement">>,<<"~> 5.12">>}],
63 - [{<<"app">>,<<"ex_money_sql">>},
64 - {<<"name">>,<<"ex_money_sql">>},
61 + {<<"requirement">>,<<">= 0.0.0">>},
62 + {<<"repository">>,<<"hexpm">>}],
63 + [{<<"name">>,<<"ex_money_sql">>},
64 + {<<"app">>,<<"ex_money_sql">>},
65 65 {<<"optional">>,false},
66 - {<<"repository">>,<<"hexpm">>},
67 - {<<"requirement">>,<<"~> 1.8">>}],
68 - [{<<"app">>,<<"paginator">>},
69 - {<<"name">>,<<"paginator">>},
66 + {<<"requirement">>,<<">= 0.0.0">>},
67 + {<<"repository">>,<<"hexpm">>}],
68 + [{<<"name">>,<<"paginator">>},
69 + {<<"app">>,<<"paginator">>},
70 70 {<<"optional">>,false},
71 - {<<"repository">>,<<"hexpm">>},
72 - {<<"requirement">>,<<"~> 1.2.0">>}],
73 - [{<<"app">>,<<"ecto_autoslug_field">>},
74 - {<<"name">>,<<"ecto_autoslug_field">>},
71 + {<<"requirement">>,<<"~> 1.2.0">>},
72 + {<<"repository">>,<<"hexpm">>}],
73 + [{<<"name">>,<<"ecto_autoslug_field">>},
74 + {<<"app">>,<<"ecto_autoslug_field">>},
75 75 {<<"optional">>,false},
76 - {<<"repository">>,<<"hexpm">>},
77 - {<<"requirement">>,<<"~> 3.0">>}],
78 - [{<<"app">>,<<"csv">>},
79 - {<<"name">>,<<"csv">>},
76 + {<<"requirement">>,<<"~> 3.0">>},
77 + {<<"repository">>,<<"hexpm">>}],
78 + [{<<"name">>,<<"csv">>},
79 + {<<"app">>,<<"csv">>},
80 80 {<<"optional">>,true},
81 - {<<"repository">>,<<"hexpm">>},
82 - {<<"requirement">>,<<"~> 3.0">>}],
83 - [{<<"app">>,<<"chromic_pdf">>},
84 - {<<"name">>,<<"chromic_pdf">>},
81 + {<<"requirement">>,<<"~> 3.0">>},
82 + {<<"repository">>,<<"hexpm">>}],
83 + [{<<"name">>,<<"chromic_pdf">>},
84 + {<<"app">>,<<"chromic_pdf">>},
85 85 {<<"optional">>,true},
86 - {<<"repository">>,<<"hexpm">>},
87 - {<<"requirement">>,<<"~> 1.7">>}]]}.
88 - {<<"version">>,<<"1.5.5">>}.
86 + {<<"requirement">>,<<"~> 1.7">>},
87 + {<<"repository">>,<<"hexpm">>}]]}.
88 + {<<"build_tools">>,[<<"mix">>]}.
  @@ -10,12 +10,6 @@ defmodule ExCatalog do
10 10
11 11 @doc """
12 12 List version.
13 -
14 - ## Examples
15 -
16 - iex> ExCatalog.version()
17 -
18 -
19 13 """
20 14 @version Mix.Project.config()[:version]
21 15 def version, do: @version
  @@ -107,6 +101,7 @@ defmodule ExCatalog do
107 101 preload: [:metas],
108 102 preload: [:primary_image],
109 103 preload: [:images],
104 + preload: [:manufacturer],
110 105 preload: [:videos]
111 106 )
112 107
  @@ -117,6 +112,7 @@ defmodule ExCatalog do
117 112 preload: [:metas],
118 113 preload: [:primary_image],
119 114 preload: [:images],
115 + preload: [:manufacturer],
120 116 preload: [:videos]
121 117 )
122 118 |> with_undeleted
  @@ -131,6 +127,7 @@ defmodule ExCatalog do
131 127 preload: [:categories],
132 128 preload: [:metas],
133 129 preload: [:primary_image],
130 + preload: [:manufacturer],
134 131 preload: [:images],
135 132 preload: [:videos]
136 133 )
  @@ -142,6 +139,7 @@ defmodule ExCatalog do
142 139 preload: [:categories],
143 140 preload: [:metas],
144 141 preload: [:primary_image],
142 + preload: [:manufacturer],
145 143 preload: [:images],
146 144 preload: [:videos]
147 145 )
  @@ -198,7 +196,9 @@ defmodule ExCatalog do
198 196
199 197 ## Examples
200 198
201 - iex> ExCatalog.product("2242", :USD)
199 + iex> price = Money.new(:USD, 100)
200 + iex> product = %{sku: "12345", price: price, title: "test product", sub_title: "test product", description: "test product"}
201 + iex> ExCatalog.product(product.sku)
202 202
203 203
204 204 """
  @@ -215,6 +215,7 @@ defmodule ExCatalog do
215 215 preload: [:categories],
216 216 preload: [:metas],
217 217 preload: [:primary_image],
218 + preload: [:manufacturer],
218 219 preload: [:images],
219 220 preload: [:videos]
220 221 )
  @@ -226,21 +227,28 @@ defmodule ExCatalog do
226 227 preload: [:categories],
227 228 preload: [:metas],
228 229 preload: [:primary_image],
230 + preload: [:manufacturer],
229 231 preload: [:images],
230 232 preload: [:videos]
231 233 )
232 234 |> with_undeleted
233 235 end
234 236
235 - [reply] = @repo.all(query)
237 + reply = @repo.all(query)
236 238
237 - case currency do
239 + case List.first(reply) do
238 240 nil ->
239 241 reply
240 242
241 - _ ->
242 - {:ok, price} = ExCatalog.Currencies.convert(reply.price, currency)
243 - %{reply | price: price}
243 + record ->
244 + case currency do
245 + nil ->
246 + record
247 +
248 + currency ->
249 + {:ok, price} = ExCatalog.Currencies.convert(record.price, currency)
250 + %{record | price: price}
251 + end
244 252 end
245 253 end
246 254
  @@ -248,8 +256,7 @@ defmodule ExCatalog do
248 256 List product with preloads by category and optional currency conversion.
249 257
250 258 ## Examples
251 -
252 - iex> ExCatalog.products_by_category("test_category", "2242", :USD, false, order_by: :sku)
259 + iex> ExCatalog.products_by_category("test_category", 1, :USD, false, order_by: :sku)
253 260
254 261
255 262 """
  @@ -258,103 +265,111 @@ defmodule ExCatalog do
258 265 end
259 266
260 267 def products_by_category(slug, limit, metadata, cursor, currency, deleted, opts) do
261 - category = @repo.get_by(%Category{}, slug: slug)
268 + category = @repo.get_by(Category, slug: slug)
262 269
263 - import Ecto.Query
264 - import Ecto.SoftDelete.Query
265 -
266 - order = Keyword.get(opts, :order) || :desc
267 - by = Keyword.get(opts, :order_by) || :updated_at
268 - order_by = [{order, by}]
269 - origins = Keyword.get(opts, :origins) || []
270 -
271 - query =
272 - case(deleted) do
273 - false ->
274 - from(p in ExCatalog.Product,
275 - where: p.category_id == ^category.id and p.origin not in ^origins,
276 - order_by: ^order_by,
277 - preload: [:variations],
278 - preload: [:categories],
279 - preload: [:metas],
280 - preload: [:primary_image],
281 - preload: [:images],
282 - preload: [:videos]
283 - )
284 -
285 - true ->
286 - from(p in ExCatalog.Product,
287 - where: p.category_id == ^category.id and p.origin not in ^origins,
288 - preload: [:variations],
289 - preload: [:categories],
290 - preload: [:metas],
291 - preload: [:primary_image],
292 - preload: [:images],
293 - preload: [:videos]
294 - )
295 - |> with_undeleted
296 - end
297 -
298 - reply =
299 - case cursor do
300 - :before ->
301 - @repo.paginate(
302 - query,
303 - before: metadata.before,
304 - include_total_count: true,
305 - cursor_fields: [:inserted_at, :id],
306 - limit: limit
307 - )
308 -
309 - :after ->
310 - @repo.paginate(
311 - query,
312 - after: metadata.after,
313 - include_total_count: true,
314 - cursor_fields: [:inserted_at, :id],
315 - limit: limit
316 - )
317 -
318 - _ ->
319 - @repo.paginate(
320 - query,
321 - include_total_count: true,
322 - cursor_fields: [:inserted_at, :id],
323 - limit: limit
324 - )
325 - end
326 -
327 - case currency do
270 + case category do
328 271 nil ->
329 - reply
272 + {:error, "Invalid Category"}
330 273
331 274 _ ->
332 - modified =
333 - Enum.map(reply.entries, fn x ->
334 - {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
335 - %{x | price: price}
336 - end)
275 + import Ecto.Query
276 + import Ecto.SoftDelete.Query
337 277
338 - {modified, reply.metadata}
278 + order = Keyword.get(opts, :order) || :desc
279 + by = Keyword.get(opts, :order_by) || :updated_at
280 + order_by = [{order, by}]
281 + origins = Keyword.get(opts, :origins) || []
282 +
283 + query =
284 + case(deleted) do
285 + false ->
286 + from(p in ExCatalog.Product,
287 + where: p.category_id == ^category.id and p.origin not in ^origins,
288 + order_by: ^order_by,
289 + preload: [:variations],
290 + preload: [:categories],
291 + preload: [:metas],
292 + preload: [:primary_image],
293 + preload: [:manufacturer],
294 + preload: [:images],
295 + preload: [:videos]
296 + )
297 +
298 + true ->
299 + from(p in ExCatalog.Product,
300 + where: p.category_id == ^category.id and p.origin not in ^origins,
301 + preload: [:variations],
302 + preload: [:categories],
303 + preload: [:metas],
304 + preload: [:primary_image],
305 + preload: [:manufacturer],
306 + preload: [:images],
307 + preload: [:videos]
308 + )
309 + |> with_undeleted
310 + end
311 +
312 + reply =
313 + case cursor do
314 + :before ->
315 + @repo.paginate(
316 + query,
317 + before: metadata.before,
318 + include_total_count: true,
319 + cursor_fields: [:inserted_at, :id],
320 + limit: limit
321 + )
322 +
323 + :after ->
324 + @repo.paginate(
325 + query,
326 + after: metadata.after,
327 + include_total_count: true,
328 + cursor_fields: [:inserted_at, :id],
329 + limit: limit
330 + )
331 +
332 + _ ->
333 + @repo.paginate(
334 + query,
335 + include_total_count: true,
336 + cursor_fields: [:inserted_at, :id],
337 + limit: limit
338 + )
339 + end
340 +
341 + case currency do
342 + nil ->
343 + reply
344 +
345 + _ ->
346 + modified =
347 + Enum.map(reply.entries, fn x ->
348 + {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
349 + %{x | price: price}
350 + end)
351 +
352 + {modified, reply.metadata}
353 + end
339 354 end
340 355 end
341 356
342 357 ###
343 358 @doc """
344 - List product with preloads by category and optional currency conversion.
359 + List product with preloads by country and optional currency conversion.
345 360
346 361 ## Examples
347 362
348 - iex> ExCatalog.products_by_category("test_category", "2242", :USD, false, order_by: :sku)
363 + iex> ExCatalog.products_by_country(["US"], 2, :USD, false, order_by: :sku)
349 364
350 365
351 366 """
352 - def products_by_country(slug, limit \\ 25, currency \\ :USD, deleted \\ false, opts \\ [])
353 - when is_list(slug) do
354 - products_by_country(slug, limit, nil, nil, currency, deleted, opts)
367 + def products_by_country(origin, limit \\ 25, currency \\ :USD, deleted \\ false, opts \\ [])
368 + when is_list(origin) do
369 + products_by_country(origin, limit, nil, nil, currency, deleted, opts)
355 370 end
356 371
357 - def products_by_country(slug, limit, metadata, cursor, currency, deleted, opts) do
372 + def products_by_country(origin, limit, metadata, cursor, currency, deleted, opts) do
358 373 import Ecto.Query
359 374 import Ecto.SoftDelete.Query
360 375
  @@ -366,23 +381,25 @@ defmodule ExCatalog do
366 381 case(deleted) do
367 382 false ->
368 383 from(p in ExCatalog.Product,
369 - where: p.slug not in ^slug,
384 + where: p.origin not in ^origin,
370 385 order_by: ^order_by,
371 386 preload: [:variations],
372 387 preload: [:categories],
373 388 preload: [:metas],
374 389 preload: [:primary_image],
390 + preload: [:manufacturer],
375 391 preload: [:images],
376 392 preload: [:videos]
377 393 )
378 394
379 395 true ->
380 396 from(p in ExCatalog.Product,
381 - where: p.slug not in ^slug,
397 + where: p.origin not in ^origin,
382 398 preload: [:variations],
383 399 preload: [:categories],
384 400 preload: [:metas],
385 401 preload: [:primary_image],
402 + preload: [:manufacturer],
386 403 preload: [:images],
387 404 preload: [:videos]
388 405 )
  @@ -434,96 +451,105 @@ defmodule ExCatalog do
434 451 end
435 452
436 453 @doc """
437 - List product with preloads by category and optional currency conversion.
454 + List product with preloads by manufacturer and optional currency conversion.
438 455
439 456 ## Examples
440 457
441 - iex> ExCatalog.products_by_category("test_category", "2242", :USD, false, order_by: :sku)
458 + iex> ExCatalog.products_by_manufacturer("test_manufacturer", 2, :USD, false, order_by: :sku)
442 459
443 460
444 461 """
462 +
445 463 def products_by_manufacturer(slug, limit \\ 25, currency \\ :USD, deleted \\ false, opts \\ []) do
446 464 products_by_manufacturer(slug, limit, nil, nil, currency, deleted, opts)
447 465 end
448 466
449 467 def products_by_manufacturer(slug, limit, metadata, cursor, currency, deleted, opts) do
450 - manufacturer = @repo.get_by(%Manufacturer{}, slug: slug)
468 + manufacturer = @repo.get_by(Manufacturer, slug: slug)
451 469
452 - import Ecto.Query
453 - import Ecto.SoftDelete.Query
454 -
455 - order = Keyword.get(opts, :order) || :desc
456 - by = Keyword.get(opts, :order_by) || :updated_at
457 - order_by = [{order, by}]
458 -
459 - query =
460 - case(deleted) do
461 - false ->
462 - from(ExCatalog.Product,
463 - where: [manufacturer: ^manufacturer],
464 - order_by: ^order_by,
465 - preload: [:variations],
466 - preload: [:categories],
467 - preload: [:metas],
468 - preload: [:primary_image],
469 - preload: [:images],
470 - preload: [:videos]
471 - )
472 -
473 - true ->
474 - from(ExCatalog.Product,
475 - where: [origin: ^slug],
476 - preload: [:variations],
477 - preload: [:categories],
478 - preload: [:metas],
479 - preload: [:primary_image],
480 - preload: [:images],
481 - preload: [:videos]
482 - )
483 - |> with_undeleted
484 - end
485 -
486 - reply =
487 - case cursor do
488 - :before ->
489 - @repo.paginate(
490 - query,
491 - before: metadata.before,
492 - include_total_count: true,
493 - cursor_fields: [:inserted_at, :id],
494 - limit: limit
495 - )
496 -
497 - :after ->
498 - @repo.paginate(
499 - query,
500 - after: metadata.after,
501 - include_total_count: true,
502 - cursor_fields: [:inserted_at, :id],
503 - limit: limit
504 - )
505 -
506 - _ ->
507 - @repo.paginate(
508 - query,
509 - include_total_count: true,
510 - cursor_fields: [:inserted_at, :id],
511 - limit: limit
512 - )
513 - end
514 -
515 - case currency do
470 + case manufacturer do
516 471 nil ->
517 - reply
472 + {:error, "Invalid Manufacturer"}
518 473
519 474 _ ->
520 - modified =
521 - Enum.map(reply.entries, fn x ->
522 - {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
523 - %{x | price: price}
524 - end)
475 + import Ecto.Query
476 + import Ecto.SoftDelete.Query
525 477
526 - {modified, reply.metadata}
478 + order = Keyword.get(opts, :order) || :desc
479 + by = Keyword.get(opts, :order_by) || :updated_at
480 + order_by = [{order, by}]
481 +
482 + query =
483 + case(deleted) do
484 + false ->
485 + from(ExCatalog.Product,
486 + where: [manufacturer: ^manufacturer],
487 + order_by: ^order_by,
488 + preload: [:variations],
489 + preload: [:categories],
490 + preload: [:metas],
491 + preload: [:primary_image],
492 + preload: [:manufacturer],
493 + preload: [:images],
494 + preload: [:videos]
495 + )
496 +
497 + true ->
498 + from(ExCatalog.Product,
499 + where: [manufacturer: ^manufacturer],
500 + preload: [:variations],
501 + preload: [:categories],
502 + preload: [:metas],
503 + preload: [:primary_image],
504 + preload: [:manufacturer],
505 + preload: [:images],
506 + preload: [:videos]
507 + )
508 + |> with_undeleted
509 + end
510 +
511 + reply =
512 + case cursor do
513 + :before ->
514 + @repo.paginate(
515 + query,
516 + before: metadata.before,
517 + include_total_count: true,
518 + cursor_fields: [:inserted_at, :id],
519 + limit: limit
520 + )
521 +
522 + :after ->
523 + @repo.paginate(
524 + query,
525 + after: metadata.after,
526 + include_total_count: true,
527 + cursor_fields: [:inserted_at, :id],
528 + limit: limit
529 + )
530 +
531 + _ ->
532 + @repo.paginate(
533 + query,
534 + include_total_count: true,
535 + cursor_fields: [:inserted_at, :id],
536 + limit: limit
537 + )
538 + end
539 +
540 + case currency do
541 + nil ->
542 + reply
543 +
544 + _ ->
545 + modified =
546 + Enum.map(reply.entries, fn x ->
547 + {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
548 + %{x | price: price}
549 + end)
550 +
551 + {modified, reply.metadata}
552 + end
527 553 end
528 554 end
529 555
  @@ -533,7 +559,7 @@ defmodule ExCatalog do
533 559
534 560 ## Examples
535 561
536 - iex> id = "1111-2222-3333-4444"
562 + iex> id = Ecto.UUID.generate
537 563 iex> ExCatalog.active(:category, id)
538 564 iex> ExCatalog.active(:product, id)
539 565
  @@ -556,8 +582,12 @@ defmodule ExCatalog do
556 582
557 583 case Keyword.has_key?(@repo.__info__(:functions), :soft_restore!) do
558 584 true ->
559 - @repo.one!(query)
560 - |> @repo.soft_restore!()
585 + try do
586 + @repo.one!(query)
587 + |> @repo.soft_restore!()
588 + rescue
589 + _ -> {:error, "Not Available"}
590 + end
561 591
562 592 false ->
563 593 {:error, "Not Available"}
  @@ -566,7 +596,6 @@ defmodule ExCatalog do
566 596
567 597 def active(type, id, false) do
568 598 import Ecto.Query
569 - import Ecto.SoftDelete.Query
570 599
571 600 case type do
572 601 :category ->
  @@ -36,6 +36,7 @@ defmodule ExCatalog.Product do
36 36 )
37 37
38 38 has_many(:images, ExCatalog.Image, on_replace: :delete)
39 + has_many(:videos, ExCatalog.Video, on_replace: :delete)
39 40 has_many(:metas, ExCatalog.Meta, on_replace: :delete)
40 41
41 42 timestamps()
  @@ -54,10 +55,11 @@ defmodule ExCatalog.Product do
54 55 :description,
55 56 :primary_image_id,
56 57 :owner_id,
57 - :origin,
58 - :manufacturer
58 + :origin
59 59 ])
60 60 |> validate_required([:sku, :price, :title, :sub_title, :description])
61 + |> TitleSlug.maybe_generate_slug()
62 + |> TitleSlug.unique_constraint()
61 63 end
62 64
63 65 @doc false
  @@ -1,7 +1,7 @@
1 1 defmodule ExCatalog.MixProject do
2 2 use Mix.Project
3 3
4 - @version "1.5.5"
4 + @version "1.5.6"
5 5 @source_url "https://github.com/mithereal/ExCatalog"
6 6
7 7 def project do
  @@ -9,13 +9,20 @@ defmodule ExCatalog.MixProject do
9 9 app: :ex_catalog,
10 10 version: @version,
11 11 elixir: "~> 1.13",
12 - build_embedded: Mix.env() == :prod,
13 12 start_permanent: Mix.env() == :prod,
13 + test_coverage: [tool: ExCoveralls],
14 + preferred_cli_env: [
15 + coveralls: :test,
16 + "coveralls.detail": :test,
17 + "coveralls.post": :test,
18 + "coveralls.html": :test
19 + ],
14 20 package: package(),
15 21 description: description(),
16 22 deps: deps(),
17 23 docs: docs(),
18 - aliases: aliases()
24 + aliases: aliases(),
25 + dialyzer: dialyzer()
19 26 ]
20 27 end
21 28
  @@ -45,6 +52,8 @@ defmodule ExCatalog.MixProject do
45 52 defp docs do
46 53 [
47 54 main: "ExCatalog",
55 + homepage_url: @source_url,
56 + source_ref: "v#{@version}",
48 57 source_url: @source_url,
49 58 extras: ["README.md"]
50 59 ]
  @@ -59,12 +68,13 @@ defmodule ExCatalog.MixProject do
59 68 {:ecto, "~> 3.5"},
60 69 {:ecto_sql, "~> 3.5"},
61 70 {:ecto_soft_delete, ">= 0.0.0"},
62 - {:ex_money, "~> 5.12"},
63 - {:ex_money_sql, "~> 1.8"},
71 + {:ex_money, ">= 0.0.0"},
72 + {:ex_money_sql, ">= 0.0.0"},
64 73 {:paginator, "~> 1.2.0"},
65 74 {:ecto_autoslug_field, "~> 3.0"},
66 75 {:csv, "~> 3.0", optional: true},
67 - {:chromic_pdf, "~> 1.7", optional: true}
76 + {:chromic_pdf, "~> 1.7", optional: true},
77 + {:excoveralls, "~> 0.14", only: [:test, :dev]}
68 78 ]
69 79 end
70 80
  @@ -82,4 +92,17 @@ defmodule ExCatalog.MixProject do
82 92 install: ["ExCatalog.install", "ecto.setup"]
83 93 ]
84 94 end
95 +
96 + defp dialyzer() do
97 + [
98 + plt_add_deps: :transitive,
99 + plt_add_apps: [:ex_unit, :mix],
100 + flags: [
101 + :error_handling,
102 + :race_conditions,
103 + :underspecs,
104 + :unmatched_returns
105 + ]
106 + ]
107 + end
85 108 end