├── .devcontainer.json ├── .editorconfig ├── .envrc ├── .gitattributes ├── .github └── workflows │ ├── docs-build.yml │ ├── fly-deploy.yml │ ├── fly-reset.yml │ ├── nightly-release.yml │ ├── promote-release.yml │ ├── tests.yml │ └── version-bump.yml ├── .gitignore ├── .misc.xml ├── LICENSE ├── Makefile ├── README.adoc ├── VERSION ├── antora-playbook.yml ├── collections ├── brands │ ├── get branch.bru │ └── get brand.bru ├── bruno.json ├── carts │ ├── add variant to cart.bru │ ├── checkout cart.bru │ ├── create cart.bru │ └── get cart.bru ├── deliveries │ ├── get delivery.bru │ ├── init delivery.bru │ ├── update delivery location.bru │ └── update delivery state.bru ├── environments │ └── localhost.bru └── products │ ├── get available products.bru │ ├── get product.bru │ ├── get products in collection.bru │ └── search available products.bru ├── devenv.local.nix.example ├── devenv.lock ├── devenv.nix ├── devenv.yaml ├── docs ├── .gitignore ├── antora.yml ├── modules │ ├── ROOT │ │ ├── nav.adoc │ │ └── pages │ │ │ ├── _example.adoc │ │ │ └── index.adoc │ ├── decisions │ │ ├── nav.adoc │ │ └── pages │ │ │ ├── 0000-use-adr.adoc │ │ │ ├── backoffice-design.adoc │ │ │ ├── use-antora-docs.adoc │ │ │ ├── use-ash.adoc │ │ │ ├── use-bruno.adoc │ │ │ ├── use-elixir-phoenix.adoc │ │ │ ├── use-golden-signals.adoc │ │ │ ├── use-grafana-prometheus.adoc │ │ │ ├── use-nix.adoc │ │ │ ├── use-tbd-and-cc.adoc │ │ │ └── use-telegram.adoc │ ├── developer │ │ ├── images │ │ │ ├── delivery_form.png │ │ │ ├── display_id.png │ │ │ ├── done_command.png │ │ │ ├── edit_delivery.png │ │ │ ├── extensions.png │ │ │ ├── live_location.png │ │ │ ├── new_command.png │ │ │ ├── update_user.png │ │ │ └── user_admin.png │ │ ├── nav.adoc │ │ └── pages │ │ │ ├── dev_environment.adoc │ │ │ ├── extensions.adoc │ │ │ ├── jj.adoc │ │ │ └── test_plan.adoc │ ├── extensions │ │ ├── nav.adoc │ │ └── pages │ │ │ └── telegram.adoc │ └── requirements │ │ ├── images │ │ ├── admin_ref.png │ │ ├── checkout_ref.png │ │ ├── graph_ref.png │ │ ├── inventory_ref.png │ │ ├── step_1.png │ │ ├── step_2.png │ │ ├── step_3.png │ │ ├── step_4.png │ │ ├── step_5.png │ │ ├── step_6.png │ │ ├── step_7.png │ │ └── workflow_notification.png │ │ ├── nav.adoc │ │ └── pages │ │ ├── personas.adoc │ │ └── prd.adoc └── supplemental-ui │ └── partials │ └── header-content.hbs ├── loki_config.yml ├── package.json ├── prometheus.yml ├── tempo_config.yml ├── tololo ├── .credo.exs ├── .dockerignore ├── .formatter.exs ├── .gitignore ├── .igniter.exs ├── Dockerfile ├── README.md ├── assets │ ├── css │ │ └── app.css │ ├── js │ │ └── app.js │ ├── tailwind.config.js │ └── vendor │ │ └── topbar.js ├── config │ ├── ci.exs │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ ├── runtime.exs │ └── test.exs ├── core │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── config.exs │ │ ├── dev.exs │ │ ├── prod.exs │ │ └── test.exs │ ├── lib │ │ ├── brands.ex │ │ ├── brands │ │ │ ├── branch.ex │ │ │ └── brand.ex │ │ ├── carts.ex │ │ ├── carts │ │ │ ├── cart.ex │ │ │ ├── cart_line.ex │ │ │ ├── discounts_calculation.ex │ │ │ └── total_calculation.ex │ │ ├── deliveries.ex │ │ ├── deliveries │ │ │ ├── actors.ex │ │ │ ├── delivery.ex │ │ │ ├── delivery_state_changes.ex │ │ │ ├── stale_cleaner.ex │ │ │ ├── transitions.ex │ │ │ └── update_history.ex │ │ ├── extension.ex │ │ ├── gettext.ex │ │ ├── kafka │ │ │ ├── ash_notifier.ex │ │ │ ├── kafka.ex │ │ │ └── noop.ex │ │ ├── location │ │ │ └── location.ex │ │ ├── products.ex │ │ └── products │ │ │ ├── add_value_to_product.ex │ │ │ ├── attribute.ex │ │ │ ├── collection.ex │ │ │ ├── collection_product.ex │ │ │ ├── discount_rule.ex │ │ │ ├── option.ex │ │ │ ├── option_value.ex │ │ │ ├── price.ex │ │ │ ├── product.ex │ │ │ ├── product_option.ex │ │ │ ├── type.ex │ │ │ ├── variant.ex │ │ │ ├── variant_generator.ex │ │ │ └── variant_option.ex │ ├── mix.exs │ ├── mix.lock │ ├── priv │ │ └── gettext │ │ │ ├── default.pot │ │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── default.po │ │ │ └── es │ │ │ └── LC_MESSAGES │ │ │ └── default.po │ └── test │ │ ├── core_test.exs │ │ └── test_helper.exs ├── extensions │ ├── kafka │ │ ├── lib │ │ │ ├── driver.ex │ │ │ ├── extension.ex │ │ │ └── supervisor.ex │ │ ├── mix.exs │ │ ├── mix.lock │ │ └── test │ │ │ ├── kafka │ │ │ └── handler.exs │ │ │ └── test_helper.exs │ ├── prometheus │ │ ├── .gitignore │ │ ├── lib │ │ │ ├── extension.ex │ │ │ ├── promex.ex │ │ │ └── promex_plugin.ex │ │ ├── mix.exs │ │ ├── mix.lock │ │ ├── priv │ │ │ └── dashboards │ │ │ │ └── deliveries.json.eex │ │ └── test │ │ │ ├── prometheus │ │ │ └── handler.exs │ │ │ └── test_helper.exs │ └── telegram_bot │ │ ├── .gitignore │ │ ├── config │ │ ├── dev.exs │ │ ├── prod.exs │ │ └── test.exs │ │ ├── lib │ │ ├── ash │ │ │ ├── user.ex │ │ │ └── users.ex │ │ ├── controller.ex │ │ ├── extension.ex │ │ ├── gettext.ex │ │ ├── kafka │ │ │ └── ash_notifier.ex │ │ ├── message.ex │ │ ├── notifier.ex │ │ └── telegram │ │ │ ├── chain_context.ex │ │ │ ├── chain_handler.ex │ │ │ ├── chains │ │ │ ├── auth.ex │ │ │ ├── done.ex │ │ │ ├── list_deliveries.ex │ │ │ ├── receive_location.ex │ │ │ └── set_token.ex │ │ │ └── handler.ex │ │ ├── mix.exs │ │ ├── mix.lock │ │ ├── priv │ │ └── gettext │ │ │ ├── default.pot │ │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── default.po │ │ │ └── es │ │ │ └── LC_MESSAGES │ │ │ └── default.po │ │ └── test │ │ ├── telegram │ │ └── handler.exs │ │ └── test_helper.exs ├── fly.toml ├── lib │ ├── gettext.ex │ ├── mix │ │ └── generate_delivery_env.ex │ ├── tololo.ex │ ├── tololo │ │ ├── accounts.ex │ │ ├── accounts │ │ │ ├── token.ex │ │ │ ├── user.ex │ │ │ └── user │ │ │ │ └── senders │ │ │ │ └── send_magic_link_email.ex │ │ ├── application.ex │ │ ├── geocoding.ex │ │ ├── mailer.ex │ │ ├── release.ex │ │ ├── repo.ex │ │ ├── request_stub.ex │ │ └── secrets.ex │ ├── tololo_web.ex │ └── tololo_web │ │ ├── auth_overrides.ex │ │ ├── components │ │ ├── core_components.ex │ │ ├── layouts.ex │ │ └── layouts │ │ │ ├── app.html.heex │ │ │ └── root.html.heex │ │ ├── controllers │ │ ├── auth_controller.ex │ │ ├── error_html.ex │ │ ├── error_json.ex │ │ ├── page_controller.ex │ │ ├── page_html.ex │ │ └── page_html │ │ │ └── home.html.heex │ │ ├── deliveries │ │ └── delivery_auth_plug.ex │ │ ├── email_templates.ex │ │ ├── endpoint.ex │ │ ├── graphql_schema.ex │ │ ├── live │ │ ├── delivery_live │ │ │ ├── form_component.ex │ │ │ ├── index.ex │ │ │ ├── location_input_component.ex │ │ │ └── show.ex │ │ ├── map_live.ex │ │ └── map_live.html.heex │ │ ├── live_user_auth.ex │ │ ├── router.ex │ │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── priv │ ├── gettext │ │ ├── default.pot │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ ├── default.po │ │ │ │ └── errors.po │ │ ├── errors.pot │ │ └── es │ │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ ├── repo │ │ ├── example_seeds.exs │ │ ├── migrations │ │ │ ├── .formatter.exs │ │ │ ├── 20250107183938_initialize_extensions_1.exs │ │ │ ├── 20250116171902_migrate_resources1.exs │ │ │ ├── 20250120201203_deliveries_extensions_1.exs │ │ │ ├── 20250121163709_add_authentication_resources_extensions_1.exs │ │ │ ├── 20250121163711_add_authentication_resources.exs │ │ │ ├── 20250121180624_deliveries_indexes.exs │ │ │ ├── 20250127131349_deliveries_current_pos.exs │ │ │ ├── 20250127135249_deliveries_auth_keys.exs │ │ │ ├── 20250131120145_deliveries_display_id.exs │ │ │ ├── 20250204002021_users.exs │ │ │ ├── 20250205234835_deliveries_on_delete.exs │ │ │ ├── 20250219172630_deliveries_nullable.exs │ │ │ ├── 20250224180533_magic_link.exs │ │ │ ├── 20250225022131_user_admin_field.exs │ │ │ ├── 20250228122847_products.exs │ │ │ ├── 20250303150336_option_identity.exs │ │ │ ├── 20250303170125_value_identity.exs │ │ │ ├── 20250303185557_variant_update.exs │ │ │ ├── 20250304111339_install_ash_money_v5_extension.exs │ │ │ ├── 20250304123947_prices.exs │ │ │ ├── 20250304135428_price_identities.exs │ │ │ ├── 20250304164127_carts.exs │ │ │ ├── 20250307094233_discount_rules.exs │ │ │ ├── 20250307145708_reference_delete.exs │ │ │ ├── 20250310183831_brand.exs │ │ │ └── 20250310194854_branch_name.exs │ │ └── seeds.exs │ ├── resource_snapshots │ │ └── repo │ │ │ ├── branches │ │ │ ├── 20250310183831.json │ │ │ └── 20250310194854.json │ │ │ ├── brands │ │ │ └── 20250310183831.json │ │ │ ├── cart_lines │ │ │ └── 20250304164127.json │ │ │ ├── carts │ │ │ └── 20250304164127.json │ │ │ ├── collection_product │ │ │ └── 20250228122847.json │ │ │ ├── collections │ │ │ └── 20250228122847.json │ │ │ ├── deliveries │ │ │ ├── 20250116171903.json │ │ │ ├── 20250127131349.json │ │ │ ├── 20250127133331.json │ │ │ ├── 20250127135249.json │ │ │ ├── 20250127173319.json │ │ │ ├── 20250131120145.json │ │ │ └── 20250219172630.json │ │ │ ├── delivery_state_changes │ │ │ ├── 20250116171903.json │ │ │ └── 20250205234835.json │ │ │ ├── extensions.json │ │ │ ├── option_values │ │ │ ├── 20250228122847.json │ │ │ ├── 20250303170125.json │ │ │ └── 20250307145708.json │ │ │ ├── options │ │ │ ├── 20250228122847.json │ │ │ ├── 20250303150336.json │ │ │ └── 20250307145708.json │ │ │ ├── prices │ │ │ ├── 20250304123947.json │ │ │ └── 20250304135428.json │ │ │ ├── product_option │ │ │ ├── 20250228122847.json │ │ │ └── 20250307145708.json │ │ │ ├── products │ │ │ ├── 20250228122847.json │ │ │ └── 20250307094233.json │ │ │ ├── representatives │ │ │ └── 20250107183939.json │ │ │ ├── telegram_bot │ │ │ └── 20250204002021.json │ │ │ ├── tickets │ │ │ └── 20250107183939.json │ │ │ ├── tokens │ │ │ └── 20250121163711.json │ │ │ ├── types │ │ │ └── 20250228122847.json │ │ │ ├── users │ │ │ ├── 20250121163711.json │ │ │ ├── 20250224180533.json │ │ │ └── 20250225022131.json │ │ │ ├── variant_option │ │ │ ├── 20250228122847.json │ │ │ └── 20250307145708.json │ │ │ └── variants │ │ │ ├── 20250228122847.json │ │ │ ├── 20250303185557.json │ │ │ └── 20250307094233.json │ ├── schema.graphql │ └── static │ │ ├── favicon.ico │ │ ├── images │ │ ├── logo.svg │ │ └── map │ │ │ ├── flatware.svg │ │ │ ├── home.svg │ │ │ └── motorcycle.svg │ │ └── robots.txt ├── rel │ ├── env.sh.eex │ └── overlays │ │ └── bin │ │ ├── migrate │ │ ├── migrate.bat │ │ ├── server │ │ └── server.bat └── test │ ├── data_case.ex │ ├── mix │ └── generate_delivery_env_test.exs │ ├── test_helper.exs │ ├── tololo │ ├── carts │ │ └── cart_test.exs │ ├── deliveries │ │ └── delivery_test.exs │ ├── geocoding_test.exs │ └── products │ │ └── product_test.exs │ └── tololo_web │ ├── conn_case.ex │ ├── controllers │ ├── error_html_test.exs │ ├── error_json_test.exs │ └── page_controller_test.exs │ ├── deliveries │ └── delivery_test.exs │ ├── gettext_test.exs │ └── live │ ├── delivery_live │ ├── form_component_test.exs │ ├── index_test.exs │ ├── location_input_component_test.exs │ ├── session.ex │ └── show_test.exs │ └── map_live_test.exs ├── vector.yaml └── yarn.lock /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "customizations": { 3 | "vscode": { 4 | "extensions": [ 5 | "mkhl.direnv" 6 | ] 7 | } 8 | }, 9 | "image": "ghcr.io/cachix/devenv:latest", 10 | "overrideCommand": false, 11 | "updateContentCommand": "devenv test" 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_size = 2 4 | indent_style = space 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | 10 | [{*.adoc, *.md}] 11 | trim_trailing_whitespace = false 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k=" 2 | 3 | use devenv 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | $ cat .gitattributes 2 | * text=auto eol=lf 3 | *.dbml linguist-language=JavaScript 4 | *.dbml linguist-vendored 5 | -------------------------------------------------------------------------------- /.github/workflows/fly-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | name: Deploy app 9 | runs-on: ubuntu-latest 10 | concurrency: deploy-group 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: superfly/flyctl-actions/setup-flyctl@master 14 | - run: flyctl deploy --remote-only 15 | working-directory: ./tololo 16 | env: 17 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/fly-reset.yml: -------------------------------------------------------------------------------- 1 | name: Fly Reset 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | deploy: 7 | name: Deploy app 8 | runs-on: ubuntu-latest 9 | concurrency: deploy-group 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: superfly/flyctl-actions/setup-flyctl@master 13 | - run: flyctl ssh console -C "/app/bin/tololo eval Tololo.Release.reset" 14 | working-directory: ./tololo 15 | env: 16 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_SSH }} 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/promote-release.yml: -------------------------------------------------------------------------------- 1 | name: Promote to Production Release 2 | # This action will create a new Production Release 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | jobs: 8 | promote-to-production: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v4 14 | 15 | - name: Create Production Release 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | run: | 19 | VERSION=$(cat VERSION) 20 | curl -L \ 21 | -X POST \ 22 | -H "Accept: application/vnd.github+json" \ 23 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 24 | -H "X-GitHub-Api-Version: 2022-11-28" \ 25 | https://api.github.com/repos/${{ github.repository }}/releases \ 26 | -d '{ 27 | "tag_name": "v'$VERSION'", 28 | "target_commitish": "main", 29 | "name": "Production Release v'$VERSION'", 30 | "body": "Manual promotion of main branch to production.", 31 | "draft": false, 32 | "prerelease": false 33 | }' 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/version-bump.yml: -------------------------------------------------------------------------------- 1 | name: Version Bump 2 | # Bump the current version following Semver MAJOR.MINOR.PATCH 3 | # Update PATCH number with every commit 4 | on: 5 | push: 6 | branches: [main] 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | version-bump: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v4 17 | 18 | - name: Increment Version 19 | id: increment_version 20 | run: | 21 | VERSION=$(cat VERSION) 22 | 23 | MAJOR=$(echo "$VERSION" | cut -d. -f1) 24 | MINOR=$(echo "$VERSION" | cut -d. -f2) 25 | PATCH=$(echo "$VERSION" | cut -d. -f3) 26 | 27 | NEW_PATCH=$((PATCH + 1)) 28 | NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" 29 | 30 | # Update the VERSION file 31 | echo "$NEW_VERSION" > VERSION 32 | 33 | # Update Version in mix.exs 34 | sed -i "0,/$VERSION/{s//$NEW_VERSION/}" tololo/mix.exs 35 | 36 | git config user.name "github-actions[bot]" 37 | git config user.email "github-actions[bot]@users.noreply.github.com" 38 | git add VERSION 39 | git add tololo/mix.exs 40 | git commit -m "chore: Bump version to $NEW_VERSION" 41 | git push 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .idea 4 | node_modules/ 5 | dbml-error.log 6 | build/ 7 | .devenv/ 8 | .devenv.flake.nix 9 | .devenv*/ 10 | .direnv/ 11 | iex.ex 12 | collections/environments/generated_env.bru 13 | devenv.local.nix 14 | .elixir_ls/ 15 | -------------------------------------------------------------------------------- /.misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs docs.server dev.shell dev.services antora.deps antora.docs mix.docs mix.docs.publish mix.deps mix.setup mix.phoenix.server mix.credo mix.format 2 | # Elixir Env 3 | ## Devenv Commands 4 | dev.shell shell dsh: 5 | @devenv shell 6 | 7 | dev.services services: 8 | @devenv up 9 | 10 | ## Mix commands 11 | mix.docs mdoc: 12 | @cd tololo && mix docs 13 | @rm -rf docs/_dist/api 14 | @mkdir -p docs/_dist/ 15 | @cp -R tololo/doc docs/_dist/api 16 | 17 | mix.docs.publish mdp: 18 | @cd tololo && mix hex.publish 19 | 20 | mix.phoenix.server mix.server mps: 21 | @cd tololo && iex -S mix phx.server | vector --config ../vector.yaml 22 | 23 | mix.credo: 24 | @cd tololo && mix credo 25 | 26 | mix.format: 27 | @cd tololo && mix format 28 | 29 | mix.deps md: 30 | @cd tololo && mix deps.get 31 | 32 | mix.setup ms: 33 | @cd tololo && mix archive.install hex phx_new 34 | @cd tololo && mix archive.install hex igniter_new 35 | @make mix.deps 36 | @cd tololo && mix ash.setup 37 | @cd tololo && mix setup 38 | 39 | # Bruno 40 | mix.gen.delivery mgd: 41 | @cd tololo && mix generate.delivery.env 42 | 43 | # Antora Env 44 | ## Antora Docs 45 | antora.docs adoc: 46 | @antora antora-playbook.yml 47 | 48 | antora.deps adeps: 49 | @yarn install 50 | 51 | # Docs 52 | docs d: 53 | @rm -rf docs/_dist 54 | @make antora.docs 55 | @make mix.docs 56 | @touch docs/_dist/.nojekyll 57 | 58 | docs.server ds: 59 | @npm run serve 60 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | // Edit in: /docs/modules/ROOT/pages/index.adoc 2 | = 🔭 Tololo E-commerce 3 | 4 | High in the mountains of the _Coquimbo_ region of https://es.wikipedia.org/wiki/Chile[Chile 🇨🇱] lies a peak called 5 | _Cerro Tololo_, whose name means "`on the edge of the abyss,`" a 6 | description given to the mountain by the ancient Diaguita people, in 7 | reference to its rugged geography on its northeastern side. 8 | 9 | - https://en.wikipedia.org/wiki/Cerro_Tololo_Inter-American_Observatory[Wikipedia] 10 | 11 | image::https://github.com/user-attachments/assets/e0908d44-0905-4240-b5ab-52e844ad7e6d[By CTIO/NOIRLab/NSF/AURA/R. Sparks - https://noirlab.edu/public/images/iotw2103a/, CC BY 4.0, https://commons.wikimedia.org/w/index.php?curid=99545008] 12 | 13 | *Tololo* is a set of _Elixir_ components that bring functionality akin 14 | to Shopify and other e-commerce platforms to 15 | https://phoenixframework.org/[Phoenix Framework]. You have complete 16 | freedom to create your own storefront(s), but we’ve already done the 17 | hard work for you in the backend. 18 | 19 | [CAUTION] 20 | ==== 21 | Version 1.x is currently in alpha release. We recommend this 22 | version for new projects, however, it is not feature-complete and 23 | therefore may not be deemed production-ready. 24 | ==== 25 | 26 | [IMPORTANT] 27 | ==== 28 | Tololo is mainly targeted to Chilean’s market needs. 29 | ==== 30 | 31 | == Features 32 | 33 | Tololo E-commerce is mainly inspired by 34 | https://github.com/lunarphp/lunar[Lunar PHP] and https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api[Shopify Storefront API] and aims to be similar in scope and features. 35 | 36 | - *Ecommerce Engine*: Based around https://ash-hq.org/[Ash Framework] 37 | models, the e-commerce core provides all the functionality you need to 38 | create an online store. 39 | 40 | - *Backoffice*: Manage your catalogue, customers and orders in our 41 | modern and extendable admin area, built in _Phoenix LiveView_. 42 | 43 | - *API*: Power your storefront or mobile app via _Tololo’s_ API. 44 | 45 | == Learning 46 | 47 | This project is not only a product that can be used in the Real World™, 48 | but is meant to be used as an example to learn how to make an 49 | professional _Elixir_ artifact. So we take great care in documentation 50 | for developers. 51 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.41 2 | -------------------------------------------------------------------------------- /antora-playbook.yml: -------------------------------------------------------------------------------- 1 | site: 2 | title: Tololo Docs 3 | url: https://elixircl.github.io/tololo 4 | start_page: "tololo::index.adoc" 5 | 6 | content: 7 | sources: 8 | - url: . 9 | branches: HEAD 10 | start_path: docs/ 11 | 12 | antora: 13 | extensions: 14 | - require: '@antora/lunr-extension' 15 | 16 | asciidoc: 17 | extensions: 18 | - asciidoctor-emoji 19 | - asciidoctor-kroki 20 | attributes: 21 | kroki-fetch-diagram: true 22 | experimental: '' 23 | idprefix: '' 24 | idseparator: '-' 25 | page-pagination: '' 26 | highlightjs-theme: monokai 27 | highlightjs-languages: js, elixir, sql, yaml 28 | 29 | ui: 30 | bundle: 31 | url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable 32 | snapshot: true 33 | # https://gitlab.com/antora/antora-ui-default/-/tree/master/src/partials 34 | supplemental_files: docs/supplemental-ui/ 35 | 36 | output: 37 | dir: docs/_dist 38 | -------------------------------------------------------------------------------- /collections/brands/get branch.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get branch 3 | type: graphql 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query GetBranch { 15 | getBranch { 16 | name 17 | address 18 | latitude 19 | longitude 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /collections/brands/get brand.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get brand 3 | type: graphql 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query GetBrand { 15 | getBrand { 16 | name 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /collections/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "Tololo GraphQL", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ], 9 | "presets": { 10 | "requestType": "graphql", 11 | "requestUrl": "{{gql_host}}:{{gql_port}}/gql" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /collections/carts/add variant to cart.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: add variant to cart 3 | type: graphql 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | mutation AddVariantToCart($cartId: ID!, $variantId: ID!, $quantity: Int) { 15 | cartAddVariant(id: $cartId, input: { variantId: $variantId, quantity: $quantity }) { 16 | result { 17 | id 18 | status 19 | cartLines { 20 | id 21 | quantity 22 | notes 23 | variant { 24 | id 25 | name 26 | } 27 | } 28 | } 29 | errors { 30 | message 31 | code 32 | } 33 | } 34 | } 35 | 36 | } 37 | 38 | body:graphql:vars { 39 | { 40 | "cartId": "01959b14-4377-7992-b534-a0f423985773", 41 | "variantId": "019581b4-ff21-75e5-82aa-f41762d61e77", 42 | "quantity": 2 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /collections/carts/checkout cart.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: checkout cart 3 | type: graphql 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | mutation CartCheckoutDelivery($cartId: ID!, $deliveryInput: JsonString!) { 15 | cartCheckoutDelivery(input: { cartId: $cartId, deliveryInput: $deliveryInput }) 16 | } 17 | 18 | } 19 | 20 | body:graphql:vars { 21 | { 22 | "cartId": "01959b14-4377-7992-b534-a0f423985773", 23 | "deliveryInput": "{\"delivery_person\": {}, \"delivery_order\": {}, \"to_name\": \"to_name\", \"to_latitude\": -33.447001713606156, \"to_longitude\": -70.65619123826207, \"to_address\": \"to_address\", \"to_phone\": \"to_phone\", \"to_notes\": \"to_notes\"}" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /collections/carts/create cart.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: create cart 3 | type: graphql 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | mutation CreateCart($currency: String) { 15 | createCart(input: { currency: $currency }) { 16 | result { 17 | id 18 | status 19 | currency 20 | } 21 | errors { 22 | message 23 | code 24 | } 25 | } 26 | } 27 | 28 | } 29 | 30 | body:graphql:vars { 31 | { 32 | "currency": "CLP" 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /collections/carts/get cart.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get cart 3 | type: graphql 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query GetCart($id: ID!) { 15 | getCart(id: $id) { 16 | id 17 | status 18 | currency 19 | cartLines { 20 | id 21 | quantity 22 | notes 23 | variant { 24 | id 25 | name 26 | sku 27 | prices { 28 | id 29 | money { 30 | amount 31 | currency 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | } 40 | 41 | body:graphql:vars { 42 | { 43 | "id": "01959a22-b124-749b-b57e-6e48c6ccf8bd" 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /collections/deliveries/get delivery.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get delivery 3 | type: graphql 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | headers { 14 | Authorization: Bearer {{public_auth_key}} 15 | } 16 | 17 | body:graphql { 18 | query ($id: ID!) { 19 | getDelivery(id: $id) { 20 | id 21 | state 22 | privateAuthKey 23 | publicAuthKey 24 | displayId 25 | deliveryPerson 26 | deliveryOrder 27 | fromLatitude 28 | fromLongitude 29 | currentLatitude 30 | currentLongitude 31 | fromName 32 | toLatitude 33 | toLongitude 34 | toName 35 | toAddress 36 | toPhone 37 | toNotes 38 | deliveryStartedAt 39 | deliveryEndedAt 40 | stateHistory { 41 | id 42 | comment 43 | } 44 | } 45 | } 46 | 47 | } 48 | 49 | body:graphql:vars { 50 | { 51 | "id": "{{id}}" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /collections/deliveries/init delivery.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: init delivery 3 | type: graphql 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | headers { 14 | Authorization: Bearer {{admin_auth_key}} 15 | } 16 | 17 | body:graphql { 18 | mutation ($input: InitDeliveryInput!) { 19 | initDelivery(input: $input) { 20 | result { 21 | id 22 | state 23 | privateAuthKey 24 | publicAuthKey 25 | displayId 26 | deliveryPerson 27 | deliveryOrder 28 | fromLatitude 29 | fromLongitude 30 | currentLatitude 31 | currentLongitude 32 | fromName 33 | toLatitude 34 | toLongitude 35 | toName 36 | toAddress 37 | toPhone 38 | toNotes 39 | deliveryStartedAt 40 | deliveryEndedAt 41 | } 42 | errors { 43 | code 44 | fields 45 | message 46 | shortMessage 47 | vars 48 | } 49 | } 50 | } 51 | 52 | } 53 | 54 | body:graphql:vars { 55 | { 56 | "input": { 57 | "delivery_person": "{\"name\": \"hola\"}", 58 | "delivery_order": "{}", 59 | "to_name": "Santiago", 60 | "to_latitude": -33.448891, 61 | "to_longitude": -70.669266, 62 | "to_address": "to_address", 63 | "to_phone": "to_phone", 64 | "to_notes": "to_notes" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /collections/deliveries/update delivery location.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: update delivery location 3 | type: graphql 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | headers { 14 | Authorization: Bearer {{private_auth_key}} 15 | } 16 | 17 | body:graphql { 18 | mutation ($id: ID!, $input: UpdateLocationInput!) { 19 | updateLocation(id: $id, input: $input) { 20 | result { 21 | id 22 | state 23 | fromLatitude 24 | fromLongitude 25 | currentLatitude 26 | currentLongitude 27 | toLatitude 28 | toLongitude 29 | } 30 | errors { 31 | code 32 | fields 33 | message 34 | shortMessage 35 | vars 36 | } 37 | } 38 | } 39 | 40 | } 41 | 42 | body:graphql:vars { 43 | { 44 | "id": "{{id}}", 45 | "input": { 46 | "currentLatitude": -33.233, 47 | "currentLongitude": -71.242 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /collections/deliveries/update delivery state.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: update delivery state 3 | type: graphql 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | headers { 14 | Authorization: Bearer {{private_auth_key}} 15 | } 16 | 17 | body:graphql { 18 | mutation ($id: ID!, $input: UpdateStateInput!) { 19 | updateState(id: $id, input: $input) { 20 | result { 21 | id 22 | state 23 | } 24 | errors { 25 | code 26 | fields 27 | message 28 | shortMessage 29 | vars 30 | } 31 | } 32 | } 33 | 34 | } 35 | 36 | body:graphql:vars { 37 | { 38 | "id": "{{id}}", 39 | "input": { 40 | "state": "In_Preparation" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /collections/environments/localhost.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | gql_host: 127.0.0.1 3 | gql_port: 4000 4 | admin_auth_key: test 5 | } 6 | -------------------------------------------------------------------------------- /collections/products/get available products.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get available products 3 | type: graphql 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query GetAvailableProducts($filter: ProductFilterInput, $sort: [ProductSortInput!]) { 15 | getAvailableProducts(filter: $filter, sort: $sort) { 16 | id 17 | name 18 | description 19 | sku 20 | state 21 | prices { 22 | id 23 | money { 24 | amount 25 | currency 26 | } 27 | } 28 | } 29 | } 30 | 31 | } 32 | 33 | body:graphql:vars { 34 | { 35 | "filter": { 36 | "name": { 37 | "like": "Gyozas al Vapor" 38 | } 39 | }, 40 | "sort": [ 41 | { 42 | "field": "NAME", 43 | "order": "ASC" 44 | } 45 | ] 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /collections/products/get product.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get product 3 | type: graphql 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query GetProduct($id: ID!) { 15 | getProduct(id: $id) { 16 | id 17 | name 18 | description 19 | sku 20 | state 21 | variants { 22 | id 23 | name 24 | sku 25 | prices { 26 | id 27 | money { 28 | amount 29 | currency 30 | } 31 | } 32 | } 33 | options { 34 | id 35 | name 36 | description 37 | optionValues { 38 | id 39 | value 40 | } 41 | } 42 | } 43 | } 44 | 45 | } 46 | 47 | body:graphql:vars { 48 | { 49 | "id": "019581b4-ff11-7551-823e-22e17158f32b" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /collections/products/get products in collection.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: get products in collection 3 | type: graphql 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query GetProductsInCollection($collectionId: ID!) { 15 | getAvailableProducts(filter: { collections: { id: { eq: $collectionId } } }) { 16 | id 17 | name 18 | description 19 | sku 20 | state 21 | collections { 22 | id 23 | name 24 | description 25 | } 26 | } 27 | } 28 | 29 | } 30 | 31 | body:graphql:vars { 32 | { 33 | "collectionId": "01959a22-b197-7d63-9a63-8990e0bd55c8" 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /collections/products/search available products.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: search available products 3 | type: graphql 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{gql_host}}:{{gql_port}}/gql 9 | body: graphql 10 | auth: none 11 | } 12 | 13 | body:graphql { 14 | query SearchAvailableProducts($searchString: String, $sort: [ProductSortInput!]) { 15 | searchAvailableProducts(searchString: $searchString, sort: $sort) { 16 | id 17 | name 18 | description 19 | sku 20 | state 21 | prices { 22 | id 23 | money { 24 | amount 25 | currency 26 | } 27 | } 28 | variants { 29 | id 30 | name 31 | sku 32 | prices { 33 | id 34 | money { 35 | amount 36 | currency 37 | } 38 | } 39 | } 40 | options { 41 | id 42 | name 43 | description 44 | optionValues { 45 | id 46 | value 47 | } 48 | } 49 | } 50 | } 51 | 52 | } 53 | 54 | body:graphql:vars { 55 | { 56 | "searchString": "Arrollados", 57 | "sort": [ 58 | { 59 | "field": "SKU", 60 | "order": "ASC" 61 | } 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /devenv.local.nix.example: -------------------------------------------------------------------------------- 1 | { pkgs, lib, inputs, ... }: 2 | 3 | { 4 | # Configure if you want Telegram (Use BotFather) 5 | env.TELEGRAM_BOT_TOKEN="abc1234"; 6 | 7 | # Use ngrok http 4000 (or similar ssh tunnel command) and pase the url with /telegram at the end 8 | # This is automatically used by local tunnels in nix, but it is here 9 | # If you want to overwrite it 10 | # env.TELEGRAM_WEBHOOK="https:///telegram"; 11 | 12 | env.LC_ALL="es_CL.UTF-8"; 13 | } 14 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | inputs: 2 | nixpkgs-unstable: 3 | url: github:NixOS/nixpkgs/nixos-unstable 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _dist/ -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: tololo 2 | version: ~ 3 | title: Tololo Ecommerce 4 | nav: 5 | - modules/ROOT/nav.adoc 6 | - modules/decisions/nav.adoc 7 | - modules/developer/nav.adoc 8 | - modules/requirements/nav.adoc 9 | - modules/extensions/nav.adoc 10 | -------------------------------------------------------------------------------- /docs/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/ROOT/nav.adoc -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | // Edit in: /docs/modules/ROOT/pages/index.adoc 2 | = 🔭 Tololo E-commerce 3 | 4 | High in the mountains of the _Coquimbo_ region of https://es.wikipedia.org/wiki/Chile[Chile 🇨🇱] lies a peak called 5 | _Cerro Tololo_, whose name means "`on the edge of the abyss,`" a 6 | description given to the mountain by the ancient Diaguita people, in 7 | reference to its rugged geography on its northeastern side. 8 | 9 | - https://en.wikipedia.org/wiki/Cerro_Tololo_Inter-American_Observatory[Wikipedia] 10 | 11 | image::https://github.com/user-attachments/assets/e0908d44-0905-4240-b5ab-52e844ad7e6d[By CTIO/NOIRLab/NSF/AURA/R. Sparks - https://noirlab.edu/public/images/iotw2103a/, CC BY 4.0, https://commons.wikimedia.org/w/index.php?curid=99545008] 12 | 13 | *Tololo* is a set of _Elixir_ components that bring functionality akin 14 | to Shopify and other e-commerce platforms to 15 | https://phoenixframework.org/[Phoenix Framework]. You have complete 16 | freedom to create your own storefront(s), but we’ve already done the 17 | hard work for you in the backend. 18 | 19 | [CAUTION] 20 | ==== 21 | Version 1.x is currently in alpha release. We recommend this 22 | version for new projects, however, it is not feature-complete and 23 | therefore may not be deemed production-ready. 24 | ==== 25 | 26 | [IMPORTANT] 27 | ==== 28 | Tololo is mainly targeted to Chilean’s market needs. 29 | ==== 30 | 31 | == Features 32 | 33 | Tololo E-commerce is mainly inspired by 34 | https://github.com/lunarphp/lunar[Lunar PHP] and https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api[Shopify Storefront API] and aims to be similar in scope and features. 35 | 36 | - *Ecommerce Engine*: Based around https://ash-hq.org/[Ash Framework] 37 | models, the e-commerce core provides all the functionality you need to 38 | create an online store. 39 | 40 | - *Backoffice*: Manage your catalogue, customers and orders in our 41 | modern and extendable admin area, built in _Phoenix LiveView_. 42 | 43 | - *API*: Power your storefront or mobile app via _Tololo’s_ API. 44 | 45 | == Learning 46 | 47 | This project is not only a product that can be used in the Real World™, 48 | but is meant to be used as an example to learn how to make an 49 | professional _Elixir_ artifact. So we take great care in documentation 50 | for developers. 51 | -------------------------------------------------------------------------------- /docs/modules/decisions/nav.adoc: -------------------------------------------------------------------------------- 1 | .Architecture Decisions 2 | * xref:0000-use-adr.adoc[] 3 | * xref:use-elixir-phoenix.adoc[] 4 | * xref:use-ash.adoc[] 5 | * xref:use-nix.adoc[] 6 | * xref:use-antora-docs.adoc[] 7 | * xref:use-tbd-and-cc.adoc[] 8 | * xref:use-bruno.adoc[] 9 | * xref:use-telegram.adoc[] 10 | * xref:use-grafana-prometheus.adoc[] 11 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/0000-use-adr.adoc: -------------------------------------------------------------------------------- 1 | = Record Architecture Decisions 2 | 3 | - Status: accepted 4 | - Date: 2025-01-06 5 | 6 | == Context 7 | 8 | We need to record the architectural decisions made on _Tololo Ecommerce_. 9 | 10 | == Decision 11 | 12 | We will use Architecture Decision Records, as 13 | http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions[described 14 | by Michael Nygard]. 15 | 16 | Examples: 17 | 18 | - https://github.com/opinionated-digital-center/architecture-decision-records/ 19 | - https://adr.github.io/madr/ 20 | 21 | == Consequences 22 | 23 | See Michael Nygard’s article, linked above. -------------------------------------------------------------------------------- /docs/modules/decisions/pages/backoffice-design.adoc: -------------------------------------------------------------------------------- 1 | === Design for backoffice 2 | 3 | - Status: accepted 4 | - Date: 2025-01-06 5 | 6 | ==== Context and Problem Statement 7 | 8 | We want to leverage existing UI libraries to quickly iterate over the admin panel of the solution while focusing on functionality over design. 9 | 10 | ==== Considered Options 11 | 12 | - https://github.com/naymspace/backpex[Backpex]: Highly customizable administration panel for Phoenix LiveView applications. Doesn't support Ash currently. 13 | - https://github.com/tfwright/live_admin[live_admin]: Low-config admin UI for Phoenix apps, built on LiveView and Ecto. 14 | - https://hexdocs.pm/ash_admin/getting-started-with-ash-admin.html[AshAdmin]: A super-admin UI dashboard for Ash Framework applications, built with Phoenix LiveView. https://elixirforum.com/t/an-exploration-of-the-the-future-of-pyro-and-ashadmin/60827/2[It's not suitable for being distributed to end-users]. 15 | - Tailwind admin dashboard template such as https://tailadmin.com/[TailAdmin]: Templates built with Tailwind, mostly just designs. 16 | 17 | ==== Decision Outcome 18 | 19 | Chosen option: "`Tailwind admin dashboard template`", because 20 | 21 | - Allows for deep customization. 22 | - Has no integration issues with the planned stack. 23 | 24 | ==== Positive Consequences 25 | 26 | Using already built Tailwind components will allow contributors focus on the functionalities rather than the design. 27 | 28 | ===== Negative Consequences 29 | 30 | Integration with the rest of the solution will need to be implemented from scratch. 31 | 32 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/use-antora-docs.adoc: -------------------------------------------------------------------------------- 1 | = Use Antora Docs 2 | 3 | - Status: accepted 4 | - Date: 2025-01-08 5 | 6 | == Context and Problem Statement 7 | 8 | We need to create all the documents for project decisions, tutorials, articles and developer documentation. 9 | A single place and tool to keep all the relevant information. 10 | 11 | == Considered Options 12 | 13 | - https://rust-lang.github.io/mdBook/[Mdbook]: Official tool for documentation creation within the _Rust_ community. A simple but powerful tool that works with _Markdown_. 14 | 15 | - https://docusaurus.io/[Docusaurus]: A documentation tool that uses _React.js_ and _Markdown_. 16 | 17 | - https://docs.antora.org/antora/latest/[Antora Docs]: A documentation tool using https://docs.asciidoctor.org[Asciidoc] as the main markup language. 18 | 19 | == Decision Outcome 20 | 21 | Chosen option: "`Antora Docs`", because: 22 | 23 | - _Asciidoc_ have a standarized markup, where _Markdown_ have different "flavours". 24 | - _Antora Docs_ creates great looking and customizable websites. 25 | - _Asciidoc_ is easily integrated with many diagramming tools (PlantUML, DBML) using https://github.com/asciidoctor/asciidoctor-kroki[Kroki]. 26 | 27 | == Consequences 28 | 29 | - _Antora Docs_ will be used as the main documentation tool for business logic and user related documents. 30 | - Low level developer _API_ documentation will be created with https://hexdocs.pm/elixir/main/writing-documentation.html[Elixir Hex Docs]. And referenced by a link to _Hexdocs_ in _Antora Docs_ generated documentation. 31 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/use-ash.adoc: -------------------------------------------------------------------------------- 1 | = Use Ash 2 | 3 | - Status: accepted 4 | - Date: 2025-01-07 5 | 6 | == Context and Problem Statement 7 | 8 | We need a backend framework that simplifies domain modeling while providing built-in support for generating both REST and GraphQL APIs, providing the flexibility needed for web/mobile users. The solution should reduce boilerplate and ensure consistency. Additionally, it should support advanced features like authorization and validation out of the box. 9 | 10 | == Considered Options 11 | 12 | - https://ash-hq.org/[Ash Framework]: A declarative, extensible framework for building Elixir applications. 13 | - Use https://hexdocs.pm/ecto/Ecto.html[Ecto] with https://phoenixframework.org/[Phoenix] directly: Better integration with the current Phoenix ecosystem, since most libraries are built around Ecto. Implementing both REST and GraphQL would greatly increase development time. 14 | - https://hexdocs.pm/sleeky/api-reference.html[Sleeky]: Lightweight alternative to Ash that's tightly coupled to Ecto. Not mature enough for use. 15 | 16 | == Decision Outcome 17 | 18 | Chosen option: "`Ash Framework`", because 19 | 20 | - Derives API endpoints from modeled resources, reducing development time. 21 | - Supports both GraphQL and REST. 22 | - Offers features like authorization and data validation without additional dependencies. 23 | 24 | == Consequences 25 | 26 | - Ash resources will be used as a source of truth to model the domain 27 | - Ash simplifies the initial development phase but might introduce challenges when integrating with some libraries. 28 | - Contributors will need to learn the Ash DSL. 29 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/use-bruno.adoc: -------------------------------------------------------------------------------- 1 | = Use Bruno 2 | 3 | - Status: accepted 4 | - Date: 2025-01-22 5 | 6 | == Context and Problem Statement 7 | 8 | We need an efficient and collaborative way to manage and share API request collections that serve as examples for our API. These collections need to support both REST and GraphQL endpoints while integrating with version control systems like Git to ensure easy collaboration and maintainability. 9 | 10 | == Considered Options 11 | 12 | - https://www.postman.com/[Postman]: It's closed-source. 13 | - https://hoppscotch.io/[Hoppscotch]: An open-source alternative, but it's not as Git-friendy due to the need for manual imports and exports of collections. 14 | 15 | == Decision Outcome 16 | 17 | Chosen option: "`Bruno`", because 18 | 19 | - Bruno provides a straightforward way to define and run API requests without unnecessary complexity. 20 | - Open-source 21 | - Supports both GraphQL and REST. 22 | - Uses local files for storing API collections, enabling integration with Git. 23 | 24 | == Consequences 25 | 26 | - The project will use Bruno to maintain API request collections directly within the repository. 27 | - Contributors can use Bruno to execute and test API requests, streamlining the development and testing process. 28 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/use-golden-signals.adoc: -------------------------------------------------------------------------------- 1 | = Use golden signals 2 | 3 | - Status: accepted 4 | - Date: 2025-01-09 5 | 6 | == Context and Problem Statement 7 | 8 | To ensure the reliability and performance of the application, we need a monitoring strategy that provides actionable insights into system health and user experience. A structured approach to identifying and addressing performance bottlenecks or failures is essential. 9 | 10 | Golden signals (latency, traffic, errors, and saturation) are widely recognized as the key indicators for monitoring distributed systems and identifying potential issues before they impact users. 11 | 12 | == Considered Options 13 | 14 | - https://sre.google/sre-book/monitoring-distributed-systems/#xref_monitoring_golden-signals[Golden Signals] (latency, traffic, errors, saturation) 15 | 16 | == Decision Outcome 17 | 18 | Chosen option: "`Golden Signals`", because 19 | 20 | - They are simple and provide actionable insights. 21 | 22 | - They help balance resource usage with effective monitoring. 23 | 24 | - They align with industry best practices for distributed systems. 25 | 26 | - They allow for proactive identification of issues and facilitate targeted alerting and troubleshooting. 27 | 28 | 29 | == Consequences 30 | 31 | Monitoring will focus on the following four signals: 32 | 33 | - Latency: Time taken to serve requests. Helps identify slow responses. 34 | 35 | - Traffic: Request rate or system load. Tracks usage trends and identifies spikes. 36 | 37 | - Errors: Rate of failed requests. Highlights application or system issues. 38 | 39 | - Saturation: Resource utilization. Indicates potential capacity problems. 40 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/use-grafana-prometheus.adoc: -------------------------------------------------------------------------------- 1 | = Use Observability and Telemetry 2 | 3 | - Status: accepted 4 | - Date: 2025-01-07 5 | 6 | == Context and Problem Statement 7 | 8 | A monitoring system that stores and visualizes system metrics will be needed to ensure the reliability and performance of the application. 9 | 10 | == Considered Options 11 | 12 | - https://grafana.com/docs/grafana/latest/getting-started/get-started-grafana-prometheus/[Grafana + Prometheus]: Grafana provides visualization and Prometheus provides metrics collection. 13 | - https://hexdocs.pm/telemetry/[Telemetry] library with https://hexdocs.pm/phoenix_live_dashboard/Phoenix.LiveDashboard.html[Phoenix LiveDashboard]: LiveDashboard lacks customizability and Telemetry doesn't persist metrics by itself. 14 | 15 | == Decision Outcome 16 | 17 | The following technologies will be used. 18 | 19 | - https://prometheus.io/[Prometheus] for event timeseries 20 | - https://grafana.com/[Grafana] for dashboard and alerts. 21 | - https://opentelemetry.io/[Open Telemetry] for service resource monitoring. 22 | - https://grafana.com/oss/tempo/[Tempo] for tracing backend. 23 | - https://grafana.com/oss/loki/[Loki] for log aggregation. 24 | - https://vector.dev/[Vector.dev] for log distribution. 25 | - https://kafka.apache.org/[Kafka] for event pipelines. 26 | 27 | == Consequences 28 | 29 | - Grafana will be used to build and maintain dashboards. 30 | - Infrastructure might require additional resources for deploying Prometheus and Grafana instances. 31 | - Grafana, Prometheus, Tempo and Kafka will be optional extensions. 32 | - Loki, Vector and Open Telemetry will be part of the core. 33 | 34 | == Links 35 | 36 | - https://hexdocs.pm/telemetry_metrics_prometheus/TelemetryMetricsPrometheus.html 37 | - https://github.com/deadtrickster/prometheus-plugs 38 | - https://github.com/deadtrickster/prometheus.ex 39 | 40 | -------------------------------------------------------------------------------- /docs/modules/decisions/pages/use-telegram.adoc: -------------------------------------------------------------------------------- 1 | = Use Telegram bot as a frontend extension 2 | 3 | - Status: accepted 4 | - Date: 2025-01-29 5 | 6 | == Context and Problem Statement 7 | 8 | We want to include a simple frontend for the backend API. It should allow a delivery person to share their live location. 9 | 10 | == Considered Options 11 | 12 | - https://core.telegram.org/bots/api[Telegram bot] 13 | - https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks[Whatsapp bot] 14 | 15 | == Decision Outcome 16 | 17 | Chosen option: "`Telegram bot`", because 18 | 19 | - Simple API with client libraries available for Elixir. 20 | - Creating a new bot with it is easy compared to Whatsapp. 21 | - Has no restrictions for free accounts. 22 | 23 | == Consequences 24 | 25 | An extension will be developed using https://github.com/telegex/telegex[Telegex], an Elixir library for interacting with the Telegram Bot API. This choice allows for seamless integration with the backend system and simplifies the task of sending and receiving messages, as well as handling location data. 26 | 27 | Since Telegram's Bot API makes it easy to request and receive live location data, the delivery person can share their location with minimal effort. It will ensure the delivery person has a straightforward and user-friendly method for sharing their location. 28 | -------------------------------------------------------------------------------- /docs/modules/developer/images/delivery_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/delivery_form.png -------------------------------------------------------------------------------- /docs/modules/developer/images/display_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/display_id.png -------------------------------------------------------------------------------- /docs/modules/developer/images/done_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/done_command.png -------------------------------------------------------------------------------- /docs/modules/developer/images/edit_delivery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/edit_delivery.png -------------------------------------------------------------------------------- /docs/modules/developer/images/extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/extensions.png -------------------------------------------------------------------------------- /docs/modules/developer/images/live_location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/live_location.png -------------------------------------------------------------------------------- /docs/modules/developer/images/new_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/new_command.png -------------------------------------------------------------------------------- /docs/modules/developer/images/update_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/update_user.png -------------------------------------------------------------------------------- /docs/modules/developer/images/user_admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/developer/images/user_admin.png -------------------------------------------------------------------------------- /docs/modules/developer/nav.adoc: -------------------------------------------------------------------------------- 1 | .Developer Docs 2 | * xref:dev_environment.adoc[] 3 | * xref:jj.adoc[] 4 | * xref:extensions.adoc[] 5 | * xref:test_plan.adoc[] 6 | -------------------------------------------------------------------------------- /docs/modules/developer/pages/extensions.adoc: -------------------------------------------------------------------------------- 1 | = Extensions 2 | 3 | Tololo is based with extensibility in mind. This means it supports optional extensions 4 | that can be enabled or disabled. For example a Telegram Bot extension that will 5 | make available a Telegram chatbot, or optional Prometheus and Grafana metrics. 6 | 7 | An extension is a traditional _Elixir_ project with its own `mix.exs` file and dependencies 8 | that is imported in the main `mix.exs` file. 9 | 10 | == Adding an extension 11 | 12 | To add an extension simply add it to the main `mix.exs` in the deps. 13 | 14 | [source, elixir] 15 | ---- 16 | {:tololo_extension_telegram_bot, 17 | path: 18 | Path.join(["extensions", "telegram_bot"]) 19 | |> Path.expand()}, 20 | ---- 21 | 22 | == Architecture 23 | 24 | The project is structured using the following components 25 | 26 | - Core: Code that is required for extensions and is shared across them. The application cannot function with out code inside core. 27 | - Extensions: Code that can be optional to be used in Main. 28 | - Main: This is the Phoenix Application that must import Core and optional Extensions. 29 | 30 | image::extensions.png[] 31 | 32 | == Extensions Capabilities 33 | 34 | An extension can have the following capabilities: 35 | 36 | - Can add new routes. 37 | - Can add new controllers. 38 | - Can add new views and liveviews. 39 | - Can have own gettext translations. 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/modules/developer/pages/jj.adoc: -------------------------------------------------------------------------------- 1 | = Using Jujutsu 2 | 3 | We recommend using https://github.com/jj-vcs/jj[_Jujutsu_] tool for working with Git repositories. Is totally optional 4 | but can make developer life easier by managing branches and merge flows. 5 | 6 | JJ is git-compatible, so from the perspective of other tools, there’s no noticeable difference between using JJ or Git. However, JJ introduces a more intuitive and streamlined workflow. 7 | 8 | == Why Use JJ? 9 | 10 | - Git Compatibility: JJ seamlessly integrates with Git, allowing you to use both tools interchangeably. 11 | 12 | - Simplified Model: JJ replaces Git’s commit-based model with `changes`, which is how a commit evolves over time, making it easier to manage and reason about changes. 13 | 14 | - Easier History Rewriting: JJ simplifies operations like rebase, amend, and reordering changes, making it more straightforward to rewrite history when needed, and leading to a cleaner history. 15 | 16 | == Learning Resources 17 | 18 | - https://jj-vcs.github.io/jj/latest/tutorial/[JJ Tutorial]: Get started with JJ using the official tutorial 19 | 20 | - https://jj-vcs.github.io/jj/latest/git-comparison/[Git Comparison]: Understand the key differences between Git and JJ with the Git comparison guide 21 | 22 | == Key Features 23 | 24 | - Changes: Changes in JJ are tracked as changes instead of commits. This simplifies the history and makes it easier to manage. 25 | 26 | - Branchless Workflow: JJ encourages a more linear workflow by minimizing the need for branches, which can reduce complexity. 27 | 28 | - Automatic changes: Every operation in JJ creates a new change, ensuring a clear and consistent history. 29 | 30 | 31 | == Getting Started 32 | 33 | To start using JJ with this project, install JJ by following the https://jj-vcs.github.io/jj/latest/install-and-setup/[installation instructions]. Then use JJ commands to interact with the repository. For example: 34 | 35 | - `jj new`: Create a new change. 36 | 37 | - `jj split`: Split the changes in a change 38 | 39 | - `jj log`: View the project’s history. 40 | 41 | - `jj rebase`: Reorder or edit changes. 42 | 43 | On first usage you may need to run `jj git init --colocate` to initialize a new Jujutsu working environment. -------------------------------------------------------------------------------- /docs/modules/extensions/nav.adoc: -------------------------------------------------------------------------------- 1 | .Extensions 2 | * xref:telegram.adoc[] 3 | -------------------------------------------------------------------------------- /docs/modules/extensions/pages/telegram.adoc: -------------------------------------------------------------------------------- 1 | = Telegram Extension 2 | 3 | This extension enables using https://telegram.org[Telegram Messenger] for Deliveries. 4 | 5 | This chatbot will communicate with the delivery person and update the map coordinates in real time 6 | for a specific delivery. 7 | 8 | == Installation 9 | 10 | Add the following code to the deps in `mix.exs` 11 | 12 | [source, elixir] 13 | ---- 14 | {:tololo_extension_telegram_bot, 15 | path: 16 | Path.join(["extensions", "telegram_bot"]) 17 | |> Path.expand()}, 18 | ---- 19 | 20 | == Available commands 21 | 22 | - `/start`: If the delivery person is not registered with the store, the store admin will authorize the delivery person in order to accept new delivery orders. 23 | 24 | - `/list`: The bot will list all the available deliveries with state `Ready to Pickup` or `In Delivery`. 25 | 26 | - `/list active`: The bot will list all the available deliveries with state `In Delivery`. 27 | 28 | - `/list queue`: The bot will list all the available deliveries with state `Ready to Pickup`. 29 | 30 | - `/new `: The delivery person accepts a delivery order and must share the location in realtime. Only will be accepted if the delivery person is within 50 meters range of the delivery starting location. The delivery must be in state `Ready to Pickup` and not be assigned to another delivery person. 31 | 32 | - `/done`: The bot will list all the `In Delivery` orders that are within the location range, assigned to the telegram user that can be ready to be `Done`. 33 | 34 | - `/done `: The delivery is marked `Done`. 35 | 36 | - `/done `: The delivery is marked `Done` with a special comment. 37 | 38 | 39 | == Configuration 40 | 41 | Expects the following environment variables: 42 | 43 | - `TELEGRAM_WEBHOOK`: a full URL, such as https://example.com/telegram. The URL must be suffixed with /telegram, because this is where the webhook route is configured. 44 | 45 | - `TELEGRAM_TOKEN`: an access token for the bot you with to use. You can obtain one https://core.telegram.org/bots/tutorial#obtain-your-bot-token[following these instructions]. 46 | 47 | If you're using `devenv`, the `TELEGRAM_WEBHOOK` variable should already be configured to be used with `localtunnel`. The `TELEGRAM_TOKEN` variable still needs to be manually added to `devenv.local.nix`. 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/modules/requirements/images/admin_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/admin_ref.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/checkout_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/checkout_ref.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/graph_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/graph_ref.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/inventory_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/inventory_ref.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_1.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_2.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_3.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_4.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_5.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_6.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/step_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/step_7.png -------------------------------------------------------------------------------- /docs/modules/requirements/images/workflow_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/docs/modules/requirements/images/workflow_notification.png -------------------------------------------------------------------------------- /docs/modules/requirements/nav.adoc: -------------------------------------------------------------------------------- 1 | .System Requirements 2 | * xref:prd.adoc[] 3 | * xref:personas.adoc[] 4 | -------------------------------------------------------------------------------- /docs/supplemental-ui/partials/header-content.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{site.title}} 5 | {{#if env.SITE_SEARCH_PROVIDER}} 6 | 7 | 8 | 9 | 10 | 11 | {{/if}} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /loki_config.yml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | grpc_listen_port: 9096 6 | 7 | common: 8 | ring: 9 | instance_addr: 127.0.0.1 10 | kvstore: 11 | store: inmemory 12 | replication_factor: 1 13 | path_prefix: .devenv/state/loki 14 | 15 | schema_config: 16 | configs: 17 | - from: 2020-05-15 18 | store: tsdb 19 | object_store: filesystem 20 | schema: v13 21 | index: 22 | prefix: index_ 23 | period: 24h 24 | 25 | storage_config: 26 | filesystem: 27 | directory: .devenv/state/loki/chunks 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tololo-docs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "private": true, 6 | "scripts": { 7 | "serve": "http-server docs/_dist/" 8 | }, 9 | "dependencies": { 10 | "@antora/lunr-extension": "^1.0.0-alpha.9", 11 | "asciidoctor-emoji": "^0.5.0", 12 | "asciidoctor-kroki": "^0.18.1", 13 | "http-server": "^14.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | # global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | - job_name: 'tololo' 23 | 24 | # metrics_path defaults to '/metrics' 25 | # scheme defaults to 'http'. 26 | 27 | static_configs: 28 | - targets: ["localhost:9090", "localhost:4000"] 29 | -------------------------------------------------------------------------------- /tempo_config.yml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 3200 3 | 4 | distributor: 5 | receivers: 6 | otlp: 7 | protocols: 8 | http: 9 | grpc: 10 | 11 | compactor: 12 | compaction: 13 | block_retention: 48h # configure total trace retention here 14 | 15 | metrics_generator: 16 | registry: 17 | external_labels: 18 | source: tempo 19 | cluster: linux-microservices 20 | storage: 21 | path: .devenv/state/tempo/wal 22 | remote_write: 23 | - url: http://localhost:9090/api/v1/write 24 | send_exemplars: true 25 | 26 | storage: 27 | trace: 28 | backend: local 29 | wal: 30 | path: .devenv/state/tempo/wal 31 | local: 32 | path: .devenv/state/tempo/local 33 | overrides: 34 | defaults: 35 | metrics_generator: 36 | processors: [service-graphs, span-metrics] 37 | -------------------------------------------------------------------------------- /tololo/.dockerignore: -------------------------------------------------------------------------------- 1 | # This file excludes paths from the Docker build context. 2 | # 3 | # By default, Docker's build context includes all files (and folders) in the 4 | # current directory. Even if a file isn't copied into the container it is still sent to 5 | # the Docker daemon. 6 | # 7 | # There are multiple reasons to exclude files from the build context: 8 | # 9 | # 1. Prevent nested folders from being copied into the container (ex: exclude 10 | # /assets/node_modules when copying /assets) 11 | # 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) 12 | # 3. Avoid sending files containing sensitive information 13 | # 14 | # More information on using .dockerignore is available here: 15 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 16 | 17 | .dockerignore 18 | 19 | # Ignore git, but keep git HEAD and refs to access current commit hash if needed: 20 | # 21 | # $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat 22 | # d0b8727759e1e0e7aa3d41707d12376e373d5ecc 23 | .git 24 | !.git/HEAD 25 | !.git/refs 26 | 27 | # Common development/test artifacts 28 | /cover/ 29 | /doc/ 30 | /test/ 31 | /tmp/ 32 | .elixir_ls 33 | 34 | # Mix artifacts 35 | /_build/ 36 | /deps/ 37 | *.ez 38 | 39 | # Generated on crash by the VM 40 | erl_crash.dump 41 | 42 | # Static artifacts - These should be fetched and built inside the Docker image 43 | /assets/node_modules/ 44 | /priv/static/assets/ 45 | /priv/static/cache_manifest.json 46 | -------------------------------------------------------------------------------- /tololo/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [ 3 | :ash_authentication_phoenix, 4 | :ash_authentication, 5 | :ash_graphql, 6 | :absinthe, 7 | :ash_postgres, 8 | :ash, 9 | :ecto, 10 | :ecto_sql, 11 | :phoenix 12 | ], 13 | subdirectories: ["priv/*/migrations"], 14 | plugins: [Absinthe.Formatter, Spark.Formatter, Phoenix.LiveView.HTMLFormatter], 15 | inputs: [ 16 | "*.{heex,ex,exs}", 17 | "{config,lib,test,extensions,core}/**/*.{heex,ex,exs}", 18 | "priv/*/*.exs" 19 | ] 20 | ] 21 | -------------------------------------------------------------------------------- /tololo/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | tololo-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | # .gitignore 39 | /priv/plts/*.plt 40 | /priv/plts/*.plt.hash -------------------------------------------------------------------------------- /tololo/.igniter.exs: -------------------------------------------------------------------------------- 1 | # This is a configuration file for igniter. 2 | # For option documentation, see https://hexdocs.pm/igniter/Igniter.Project.IgniterConfig.html 3 | # To keep it up to date, use `mix igniter.setup` 4 | [ 5 | module_location: :outside_matching_folder, 6 | extensions: [{Igniter.Extensions.Phoenix, []}], 7 | source_folders: ["lib", "test/support"], 8 | dont_move_files: [~r"lib/mix"] 9 | ] 10 | -------------------------------------------------------------------------------- /tololo/README.md: -------------------------------------------------------------------------------- 1 | # Tololo 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 | 12 | ## Learn more 13 | 14 | * Official website: https://www.phoenixframework.org/ 15 | * Guides: https://hexdocs.pm/phoenix/overview.html 16 | * Docs: https://hexdocs.pm/phoenix 17 | * Forum: https://elixirforum.com/c/phoenix-forum 18 | * Source: https://github.com/phoenixframework/phoenix 19 | -------------------------------------------------------------------------------- /tololo/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | -------------------------------------------------------------------------------- /tololo/core/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /tololo/core/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | core-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /tololo/core/README.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `core` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:core, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /tololo/core/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | config :ash, known_types: [AshMoney.Types.Money], custom_types: [money: AshMoney.Types.Money] 3 | config :ex_cldr, default_backend: TololoCore.Cldr 4 | import_config "#{config_env()}.exs" 5 | -------------------------------------------------------------------------------- /tololo/core/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /tololo/core/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /tololo/core/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /tololo/core/lib/brands.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Brands do 2 | @moduledoc """ 3 | Store the Brand associated. 4 | """ 5 | 6 | use Ash.Domain, 7 | otp_app: :tololo, 8 | extensions: [AshGraphql, AshAdmin.Domain], 9 | validate_config_inclusion?: false 10 | 11 | admin do 12 | show?(true) 13 | show_resources([TololoCore.Brands.Brand, TololoCore.Brands.Branch]) 14 | end 15 | 16 | resources do 17 | resource TololoCore.Brands.Brand 18 | resource TololoCore.Brands.Branch 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /tololo/core/lib/brands/brand.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Brands.Brand do 2 | @moduledoc """ 3 | Define the data fields for storing store brand information. 4 | """ 5 | 6 | use Ash.Resource, 7 | otp_app: :tololo, 8 | domain: TololoCore.Brands, 9 | extensions: [AshGraphql.Resource, AshAdmin.Resource], 10 | data_layer: AshPostgres.DataLayer, 11 | authorizers: [Ash.Policy.Authorizer], 12 | notifiers: [TololoCore.Kafka.AshNotifier] 13 | 14 | use Gettext, backend: TololoCore.Gettext 15 | alias Ash.Changeset 16 | 17 | graphql do 18 | type :brand 19 | 20 | queries do 21 | read_one :get_brand, :read 22 | end 23 | 24 | mutations do 25 | end 26 | end 27 | 28 | admin do 29 | end 30 | 31 | postgres do 32 | table "brands" 33 | repo Tololo.Repo 34 | 35 | references do 36 | end 37 | end 38 | 39 | field_policies do 40 | field_policy :* do 41 | description "the rest of the fields don't require any special policies" 42 | authorize_if always() 43 | end 44 | end 45 | 46 | code_interface do 47 | define :create 48 | define :update 49 | end 50 | 51 | actions do 52 | defaults [:read, :destroy, create: :*, update: :*] 53 | end 54 | 55 | policies do 56 | bypass always() do 57 | description "admin has access to every action" 58 | authorize_if actor_attribute_equals(:access_level, :admin) 59 | end 60 | 61 | policy action_type(:read) do 62 | description "read access is always allowed" 63 | authorize_if always() 64 | end 65 | end 66 | 67 | attributes do 68 | uuid_v7_primary_key :id 69 | 70 | attribute :name, :string do 71 | allow_nil? false 72 | sensitive? false 73 | public? true 74 | end 75 | 76 | # Define a Headquarters Tololo URL to obtain 77 | # Common brand and products information 78 | attribute :hq_url, :string do 79 | allow_nil? true 80 | sensitive? true 81 | public? false 82 | end 83 | 84 | timestamps() 85 | end 86 | 87 | relationships do 88 | has_one :branch, TololoCore.Brands.Branch do 89 | public? true 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /tololo/core/lib/carts.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Carts do 2 | @moduledoc """ 3 | Domain that contains resources related to the cart system. 4 | """ 5 | use Ash.Domain, 6 | otp_app: :tololo, 7 | extensions: [AshGraphql.Domain], 8 | validate_config_inclusion?: false 9 | 10 | resources do 11 | resource TololoCore.Carts.Cart 12 | resource TololoCore.Carts.CartLine 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /tololo/core/lib/carts/cart_line.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Carts.CartLine do 2 | # @moduledoc """ 3 | 4 | # """ 5 | use Ash.Resource, 6 | otp_app: :tololo, 7 | domain: TololoCore.Carts, 8 | extensions: [AshGraphql.Resource], 9 | data_layer: AshPostgres.DataLayer, 10 | authorizers: [Ash.Policy.Authorizer], 11 | notifiers: [Ash.Notifier.PubSub, TololoCore.Kafka.AshNotifier] 12 | 13 | graphql do 14 | type :cart_line 15 | end 16 | 17 | postgres do 18 | table "cart_lines" 19 | repo Tololo.Repo 20 | end 21 | 22 | field_policies do 23 | field_policy :* do 24 | description "the rest of the fields don't require any special policies" 25 | authorize_if always() 26 | end 27 | end 28 | 29 | actions do 30 | defaults [:read, :destroy, update: :*] 31 | 32 | create :create do 33 | primary? true 34 | upsert? true 35 | upsert_identity :unique_variant 36 | accept :* 37 | end 38 | end 39 | 40 | policies do 41 | bypass always() do 42 | authorize_if always() 43 | end 44 | end 45 | 46 | attributes do 47 | uuid_v7_primary_key :id 48 | 49 | attribute :quantity, :integer, public?: true, allow_nil?: false, constraints: [min: 1] 50 | attribute :notes, :string, public?: true 51 | 52 | timestamps() 53 | end 54 | 55 | relationships do 56 | belongs_to :variant, TololoCore.Products.Variant, public?: true, allow_nil?: false 57 | belongs_to :cart, TololoCore.Carts.Cart, public?: true, allow_nil?: false 58 | end 59 | 60 | calculations do 61 | calculate :subtotal_before_discount, 62 | :money, 63 | expr( 64 | first(variant.prices, 65 | query: [ 66 | filter: currency == variant.cart_lines.cart.currency, 67 | load: [variant: [cart_lines: [:cart]]] 68 | ], 69 | field: :money 70 | ) * 71 | quantity 72 | ) 73 | 74 | calculate :subtotal, 75 | :money, 76 | TololoCore.Carts.DiscountsCalculation 77 | end 78 | 79 | identities do 80 | identity :unique_variant, [:variant_id, :cart_id] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /tololo/core/lib/deliveries.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Deliveries do 2 | @moduledoc """ 3 | Domain that contains resources related to the delivery system. 4 | """ 5 | use Ash.Domain, 6 | otp_app: :tololo, 7 | extensions: [AshGraphql.Domain, AshAdmin.Domain], 8 | validate_config_inclusion?: false 9 | 10 | admin do 11 | show?(true) 12 | show_resources(TololoCore.Deliveries.Delivery) 13 | end 14 | 15 | resources do 16 | resource TololoCore.Deliveries.Delivery 17 | resource TololoCore.Deliveries.DeliveryStateChanges 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /tololo/core/lib/deliveries/actors.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Deliveries.Actors do 2 | @moduledoc """ 3 | Defines actors used in the Deliveries domain. 4 | """ 5 | def admin, do: %{access_level: :admin} 6 | def private, do: %{access_level: :private} 7 | def public, do: %{access_level: :public} 8 | end 9 | -------------------------------------------------------------------------------- /tololo/core/lib/deliveries/delivery_state_changes.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Deliveries.DeliveryStateChanges do 2 | @moduledoc """ 3 | Resource that stores the state changes of a delivery. 4 | """ 5 | use Ash.Resource, 6 | otp_app: :tololo, 7 | domain: TololoCore.Deliveries, 8 | extensions: [AshGraphql.Resource], 9 | data_layer: AshPostgres.DataLayer 10 | 11 | graphql do 12 | type :delivery_state_changes 13 | end 14 | 15 | postgres do 16 | table "delivery_state_changes" 17 | repo Tololo.Repo 18 | 19 | references do 20 | reference :delivery, on_delete: :delete 21 | end 22 | end 23 | 24 | code_interface do 25 | define :add_to_state_history, 26 | args: [:delivery_id, :old_state, :new_state, :comment], 27 | action: :create 28 | end 29 | 30 | actions do 31 | defaults [:read, :create, :destroy] 32 | default_accept [:delivery_id, :old_state, :new_state, :comment] 33 | end 34 | 35 | attributes do 36 | uuid_v7_primary_key :id 37 | 38 | attribute :old_state, :string do 39 | public? true 40 | end 41 | 42 | attribute :new_state, :string do 43 | allow_nil? false 44 | public? true 45 | end 46 | 47 | attribute :comment, :string do 48 | public? true 49 | end 50 | 51 | timestamps() 52 | end 53 | 54 | relationships do 55 | belongs_to :delivery, TololoCore.Deliveries.Delivery 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /tololo/core/lib/deliveries/update_history.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Deliveries.UpdateHistory do 2 | @moduledoc """ 3 | Checks validity of state transition and also adds it to the state history. 4 | """ 5 | 6 | alias TololoCore.Deliveries.Transitions 7 | alias TololoCore.Deliveries.DeliveryStateChanges 8 | 9 | use Ash.Resource.Change 10 | use Gettext, backend: TololoCore.Gettext 11 | 12 | @impl true 13 | @spec change(Ash.Changeset.t(), term(), term()) :: nil 14 | def change(changeset, _opts, _context) do 15 | %{id: id, state: old_state} = changeset.data 16 | 17 | with {:ok, new_state} <- Ash.Changeset.fetch_change(changeset, :state), 18 | true <- Transitions.valid?(old_state, new_state) do 19 | comment = Transitions.message(old_state, new_state) 20 | 21 | changeset 22 | |> Ash.Changeset.after_transaction(fn 23 | _changeset, {:ok, result} -> 24 | DeliveryStateChanges.add_to_state_history!(id, old_state, new_state, comment) 25 | 26 | :telemetry.execute([:ash, :deliveries, :update, :state], %{count: 1}, %{ 27 | action: :update_state, 28 | old_state: old_state, 29 | new_state: new_state 30 | }) 31 | 32 | {:ok, result} 33 | 34 | _changeset, error -> 35 | error 36 | end) 37 | else 38 | _ -> 39 | changeset 40 | |> Ash.Changeset.add_error( 41 | field: :state, 42 | message: gettext("Invalid delivery state transition") 43 | ) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /tololo/core/lib/extension.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Extension do 2 | @moduledoc """ 3 | Defines the extension behaviour. 4 | """ 5 | @callback routes() :: Macro.t() 6 | @callback endpoint() :: Macro.t() 7 | @callback ash_domains() :: [Ash.Domain.t()] 8 | @callback child_spec(any()) :: Supervisor.child_spec() 9 | # @callback payment() 10 | 11 | @extensions Application.compile_env(:tololo, :extensions) 12 | 13 | defmacro __using__([]) do 14 | quote do 15 | @behaviour TololoCore.Extension 16 | 17 | @before_compile TololoCore.Extension 18 | end 19 | end 20 | 21 | defmacro __using__(:routes) do 22 | quote do 23 | unquote( 24 | @extensions 25 | |> Enum.map(fn extension_module -> extension_module.routes() end) 26 | ) 27 | end 28 | end 29 | 30 | defmacro __using__(:endpoint) do 31 | quote do 32 | unquote( 33 | @extensions 34 | |> Enum.map(fn extension_module -> extension_module.endpoint() end) 35 | ) 36 | end 37 | end 38 | 39 | @doc false 40 | defmacro __before_compile__(env) do 41 | functions = Module.definitions_in(env.module) 42 | 43 | quote do 44 | if not Enum.member?(unquote(functions), {:routes, 0}) do 45 | @impl true 46 | def routes, do: [] 47 | end 48 | 49 | if not Enum.member?(unquote(functions), {:endpoint, 0}) do 50 | @impl true 51 | def endpoint do 52 | quote do 53 | end 54 | end 55 | end 56 | 57 | if not Enum.member?(unquote(functions), {:ash_domains, 0}) do 58 | @impl true 59 | def ash_domains, do: [] 60 | end 61 | 62 | if not Enum.member?(unquote(functions), {:child_spec, 1}) do 63 | @impl true 64 | def child_spec(init_arg), 65 | do: %{ 66 | id: __MODULE__, 67 | start: {__MODULE__, :start_link, [init_arg]} 68 | } 69 | end 70 | 71 | if not Enum.member?(unquote(functions), {:start_link, 1}) do 72 | def start_link(_), do: :ignore 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /tololo/core/lib/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.CldrConfig do 2 | @moduledoc """ 3 | Config for CLDR. 4 | """ 5 | def common(gettext), 6 | do: [ 7 | locales: ["en", "es"], 8 | default_locale: "es", 9 | providers: [Cldr.Number, Cldr.DateTime, Cldr.Unit, Cldr.List, Cldr.Calendar, Cldr.Message], 10 | gettext: gettext, 11 | message_formats: %{ 12 | USD: [format: :long] 13 | } 14 | ] 15 | end 16 | 17 | defmodule TololoCore.Cldr do 18 | @moduledoc """ 19 | Config for CLDR. 20 | """ 21 | use Cldr, TololoCore.CldrConfig.common(TololoCore.Gettext) 22 | end 23 | 24 | defmodule TololoCore.Gettext.Interpolation do 25 | @moduledoc """ 26 | Define an interpolation module for ICU messages 27 | """ 28 | use Cldr.Gettext.Interpolation, cldr_backend: TololoCore.Cldr 29 | end 30 | 31 | defmodule TololoCore.Gettext do 32 | @moduledoc """ 33 | A module providing Internationalization with a gettext-based API. 34 | 35 | By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations 36 | that you can use in your application. To use this Gettext backend module, 37 | call `use Gettext` and pass it as an option: 38 | 39 | use Gettext, backend: TololoCore.Gettext 40 | 41 | # Simple translation 42 | gettext("Here is the string to translate") 43 | 44 | # Plural translation 45 | ngettext("Here is the string to translate", 46 | "Here are the strings to translate", 47 | 3) 48 | 49 | # Domain-based translation 50 | dgettext("errors", "Here is the error message to translate") 51 | 52 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 53 | """ 54 | use Gettext.Backend, otp_app: :tololo_core, interpolation: TololoCore.Gettext.Interpolation 55 | end 56 | -------------------------------------------------------------------------------- /tololo/core/lib/kafka/ash_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Kafka.AshNotifier do 2 | use Ash.Notifier 3 | alias Ash.Notifier.Notification 4 | 5 | def notify(%Notification{ 6 | data: resource, 7 | action: %{name: :initialize} 8 | }) do 9 | produce( 10 | resource, 11 | %{event: "delivery_initialized"} 12 | ) 13 | end 14 | 15 | def notify(%Notification{ 16 | data: resource, 17 | changeset: %{data: %{state: old_state}}, 18 | action: %{name: :update_state} 19 | }) do 20 | produce( 21 | resource, 22 | %{event: "delivery_state_change", old_state: old_state} 23 | ) 24 | end 25 | 26 | def notify(%Notification{ 27 | data: resource, 28 | changeset: %{data: %{state: old_state}}, 29 | action: %{name: :done_with_distance_check} 30 | }) do 31 | produce( 32 | resource, 33 | %{event: "delivery_state_change", old_state: old_state} 34 | ) 35 | end 36 | 37 | def notify(_notif), do: nil 38 | 39 | defp produce(resource, data), 40 | do: TololoCore.Kafka.produce("tololo-deliveries", data |> Map.put(:resource, resource)) 41 | end 42 | -------------------------------------------------------------------------------- /tololo/core/lib/kafka/kafka.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Kafka do 2 | @moduledoc """ 3 | A kafka interface to send messages. 4 | """ 5 | 6 | # env put by Kafka extension 7 | defp driver, do: Application.get_env(:tololo, :kafka_driver, TololoCore.Kafka.Noop) 8 | 9 | @doc """ 10 | Produces a message to Kafka. Should be implemented by drivers. 11 | """ 12 | 13 | @callback produce(String.t(), String.t() | map(), term()) :: :ok | {:error, term()} 14 | 15 | @spec produce(String.t(), String.t() | map(), term()) :: :ok | {:error, term()} 16 | def produce(topic, message, opts \\ []) 17 | 18 | def produce(topic, message, opts) when is_map(message), 19 | do: produce(topic, Jason.encode!(message), opts) 20 | 21 | def produce(topic, message, opts), do: driver().produce(topic, message, opts) 22 | end 23 | -------------------------------------------------------------------------------- /tololo/core/lib/kafka/noop.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Kafka.Noop do 2 | @moduledoc """ 3 | Noop driver implementation for Kafka. Used when Kafka isn't configured, will always return :ok. 4 | """ 5 | 6 | @behaviour TololoCore.Kafka 7 | 8 | @spec produce(String.t(), String.t(), term()) :: :ok | {:error, term()} 9 | def produce(_topic, _message, _opts \\ []), do: :ok 10 | end 11 | -------------------------------------------------------------------------------- /tololo/core/lib/location/location.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Location do 2 | @moduledoc """ 3 | Contains location functions and useful data structures for geoposition 4 | """ 5 | 6 | @earth_radius_in_meters 6_371_000 7 | 8 | @spec distance({float(), float()}, {float(), float()}) :: float() 9 | @doc """ 10 | Haversine formula is the most common method for calculating distances between geopoints, 11 | providing accurate results for most scenarios 12 | 13 | - See: [Wikipedia](https://en.wikipedia.org/wiki/Haversine_formula#:~:text=The%20law%20of%20haversines,-Spherical%20triangle%20solved&text=Since%20this%20is%20a%20unit,radius%20R%20of%20the%20sphere).) 14 | 15 | """ 16 | def distance({lat1, lon1}, {lat2, lon2}) do 17 | # You need the latitude and longitude of both points in decimal degrees 18 | dlat = deg2rad(lat2 - lat1) 19 | dlon = deg2rad(lon2 - lon1) 20 | 21 | a = 22 | :math.pow(:math.sin(dlat / 2), 2) + 23 | :math.cos(deg2rad(lat1)) * :math.cos(deg2rad(lat2)) * 24 | :math.pow(:math.sin(dlon / 2), 2) 25 | 26 | c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a)) 27 | 28 | # To convert the calculated distance to meters, you need to multiply the result by the Earth's average radius (approximately 6,371,000 meters) 29 | @earth_radius_in_meters * c 30 | end 31 | 32 | # Convert degrees to radians 33 | defp deg2rad(deg), do: deg * :math.pi() / 180 34 | end 35 | -------------------------------------------------------------------------------- /tololo/core/lib/products.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products do 2 | @moduledoc """ 3 | Domain that contains resources related to product management. 4 | """ 5 | use Ash.Domain, 6 | otp_app: :tololo, 7 | extensions: [AshGraphql.Domain, AshAdmin.Domain], 8 | validate_config_inclusion?: false 9 | 10 | admin do 11 | show?(true) 12 | show_resources(TololoCore.Products.Product) 13 | end 14 | 15 | resources do 16 | resource TololoCore.Products.Product 17 | resource TololoCore.Products.ProductOption 18 | 19 | resource TololoCore.Products.Variant 20 | resource TololoCore.Products.VariantOption 21 | 22 | resource TololoCore.Products.CollectionProduct 23 | resource TololoCore.Products.Collection 24 | 25 | resource TololoCore.Products.Option 26 | resource TololoCore.Products.OptionValue 27 | 28 | resource TololoCore.Products.Price 29 | 30 | resource TololoCore.Products.Type 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /tololo/core/lib/products/add_value_to_product.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.AddValueToProduct do 2 | use Ash.Resource.ManualUpdate 3 | 4 | def update(%{arguments: %{option: option, value: value}} = changeset, _, _) do 5 | with {:ok, %{options: options} = record} <- changeset |> ensure_option_exists(option), 6 | option_record when not is_nil(option_record) <- Enum.find(options, &(&1.name == option)), 7 | {:ok, _} <- 8 | TololoCore.Products.Option.add_value(option_record, %{value: value}, authorize?: false) do 9 | {:ok, record} 10 | else 11 | nil -> {:error, :option_not_found} 12 | {:error, error} -> {:error, error} 13 | end 14 | end 15 | 16 | defp ensure_option_exists(changeset, option) do 17 | changeset 18 | |> Ash.Changeset.manage_relationship(:options, %{name: option}, type: :create) 19 | |> Ash.Changeset.load(:options) 20 | |> Ash.update(action: :update) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /tololo/core/lib/products/attribute.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.Attribute do 2 | # @moduledoc """ 3 | 4 | # """ 5 | use Ash.Resource, 6 | domain: TololoCore.Products, 7 | extensions: [AshGraphql.Resource], 8 | data_layer: :embedded 9 | 10 | graphql do 11 | type :attributes 12 | end 13 | 14 | attributes do 15 | uuid_v7_primary_key :id 16 | 17 | attribute :name, :string, public?: true, allow_nil?: false 18 | attribute :description, :string, public?: true, allow_nil?: false 19 | attribute :sort, :integer, public?: true, default: 0 20 | attribute :handle, :string, public?: true, allow_nil?: false 21 | attribute :type, :string, public?: true, default: "string" 22 | attribute :section, :string, public?: true, allow_nil?: false 23 | attribute :required, :boolean, public?: true, default: false 24 | attribute :default_value, :string, public?: true 25 | attribute :configuration, :map, default: %{} 26 | 27 | timestamps() 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /tololo/core/lib/products/collection_product.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.CollectionProduct do 2 | @moduledoc """ 3 | Join resource for the collections and products many_to_many relationship. 4 | """ 5 | use Ash.Resource, 6 | domain: TololoCore.Products, 7 | data_layer: AshPostgres.DataLayer 8 | 9 | postgres do 10 | table "collection_product" 11 | repo Tololo.Repo 12 | end 13 | 14 | actions do 15 | defaults [:read, :destroy, create: :*, update: :*] 16 | end 17 | 18 | relationships do 19 | belongs_to :product, TololoCore.Products.Product do 20 | primary_key? true 21 | allow_nil? false 22 | public? true 23 | end 24 | 25 | belongs_to :collection, TololoCore.Products.Collection do 26 | primary_key? true 27 | allow_nil? false 28 | public? true 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /tololo/core/lib/products/discount_rule.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.DiscountRule do 2 | # @moduledoc """ 3 | 4 | # """ 5 | use Ash.Resource, 6 | domain: TololoCore.Products, 7 | extensions: [AshGraphql.Resource], 8 | data_layer: :embedded 9 | 10 | graphql do 11 | type :attributes 12 | end 13 | 14 | attributes do 15 | uuid_v7_primary_key :id 16 | 17 | attribute :method, :atom do 18 | public? true 19 | allow_nil? false 20 | constraints one_of: [:fixed, :percentage, :x_for_y, :x_for_fixed_price] 21 | end 22 | 23 | attribute :method_data, :map do 24 | public? true 25 | default %{} 26 | end 27 | 28 | attribute :discount, :decimal, public?: true 29 | attribute :enabled, :boolean, public?: true, default: true 30 | attribute :conditions, {:array, :map}, public?: true, default: [] 31 | 32 | # attribute :currencies | TODO make rules specific to currencies. if no currencies are provided, it applies to all of them 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /tololo/core/lib/products/option_value.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.OptionValue do 2 | # @moduledoc """ 3 | 4 | # """ 5 | use Ash.Resource, 6 | otp_app: :tololo, 7 | domain: TololoCore.Products, 8 | extensions: [AshGraphql.Resource, AshAdmin.Resource], 9 | data_layer: AshPostgres.DataLayer, 10 | authorizers: [Ash.Policy.Authorizer], 11 | notifiers: [TololoCore.Kafka.AshNotifier] 12 | 13 | graphql do 14 | type :option_value 15 | end 16 | 17 | postgres do 18 | table "option_values" 19 | repo Tololo.Repo 20 | 21 | references do 22 | reference :variant, on_delete: :delete 23 | reference :option, on_delete: :delete 24 | end 25 | end 26 | 27 | field_policies do 28 | field_policy :* do 29 | description "the rest of the fields don't require any special policies" 30 | authorize_if always() 31 | end 32 | end 33 | 34 | code_interface do 35 | end 36 | 37 | actions do 38 | defaults [:read, :destroy, create: :*, update: :*] 39 | end 40 | 41 | policies do 42 | bypass always() do 43 | description "admin has access to every action" 44 | authorize_if actor_attribute_equals(:access_level, :admin) 45 | end 46 | 47 | policy action_type(:read) do 48 | description "read access is always allowed" 49 | authorize_if always() 50 | end 51 | end 52 | 53 | attributes do 54 | uuid_v7_primary_key :id 55 | 56 | attribute :value, :string do 57 | allow_nil? false 58 | public? true 59 | end 60 | 61 | timestamps() 62 | end 63 | 64 | relationships do 65 | belongs_to :option, TololoCore.Products.Option do 66 | public? true 67 | end 68 | 69 | belongs_to :variant, TololoCore.Products.Variant do 70 | public? true 71 | end 72 | 73 | identities do 74 | identity :unique_value, [:value, :option_id] 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /tololo/core/lib/products/product_option.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.ProductOption do 2 | @moduledoc """ 3 | Join resource for the options and products many_to_many relationship. 4 | """ 5 | use Ash.Resource, 6 | domain: TololoCore.Products, 7 | data_layer: AshPostgres.DataLayer 8 | 9 | postgres do 10 | table "product_option" 11 | repo Tololo.Repo 12 | 13 | references do 14 | reference :product, on_delete: :delete 15 | reference :option, on_delete: :delete 16 | end 17 | end 18 | 19 | actions do 20 | defaults [:read, :destroy, create: :*, update: :*] 21 | end 22 | 23 | relationships do 24 | belongs_to :product, TololoCore.Products.Product do 25 | primary_key? true 26 | allow_nil? false 27 | public? true 28 | end 29 | 30 | belongs_to :option, TololoCore.Products.Option do 31 | primary_key? true 32 | allow_nil? false 33 | public? true 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /tololo/core/lib/products/type.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.Type do 2 | # @moduledoc """ 3 | 4 | # """ 5 | use Ash.Resource, 6 | otp_app: :tololo, 7 | domain: TololoCore.Products, 8 | extensions: [AshGraphql.Resource, AshAdmin.Resource], 9 | data_layer: AshPostgres.DataLayer, 10 | authorizers: [Ash.Policy.Authorizer], 11 | notifiers: [TololoCore.Kafka.AshNotifier] 12 | 13 | graphql do 14 | type :type 15 | end 16 | 17 | postgres do 18 | table "types" 19 | repo Tololo.Repo 20 | end 21 | 22 | field_policies do 23 | field_policy :* do 24 | description "the rest of the fields don't require any special policies" 25 | authorize_if always() 26 | end 27 | end 28 | 29 | actions do 30 | defaults [:read, :destroy, create: :*, update: :*] 31 | end 32 | 33 | policies do 34 | bypass always() do 35 | description "admin has access to every action" 36 | authorize_if actor_attribute_equals(:access_level, :admin) 37 | end 38 | 39 | policy action_type(:read) do 40 | description "read access is always allowed" 41 | authorize_if always() 42 | end 43 | end 44 | 45 | attributes do 46 | uuid_v7_primary_key :id 47 | 48 | attribute :name, :string do 49 | allow_nil? false 50 | public? true 51 | end 52 | 53 | attribute :description, :string do 54 | public? true 55 | end 56 | 57 | attribute :attributes, {:array, TololoCore.Products.Attribute} do 58 | public? true 59 | end 60 | 61 | timestamps() 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /tololo/core/lib/products/variant_generator.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.Product.VariantGenerator do 2 | @moduledoc """ 3 | Generates variants for a product, using its options. 4 | """ 5 | use Ash.Resource.Change 6 | 7 | @impl true 8 | @spec change(Ash.Changeset.t(), term(), term()) :: Ash.Changeset.t() 9 | def change( 10 | %{ 11 | data: %{ 12 | options: options, 13 | name: name, 14 | description: description, 15 | sku: sku, 16 | prices: prices, 17 | discount_rules: discount_rules 18 | } 19 | } = changeset, 20 | _opts, 21 | _context 22 | ) do 23 | options 24 | |> cartesian_product() 25 | |> Enum.map(fn combination -> 26 | prices = 27 | if is_list(prices), 28 | do: 29 | prices 30 | |> Enum.map(fn %{money: money} -> 31 | %{money: %{amount: money.amount, currency: money.currency}} 32 | end), 33 | else: [] 34 | 35 | TololoCore.Products.Variant 36 | |> Ash.Changeset.for_create( 37 | :create, 38 | %{ 39 | option_value_ids: combination |> Enum.map(& &1.id), 40 | name: name, 41 | description: description, 42 | sku: sku, 43 | discount_rules: discount_rules 44 | }, 45 | authorize?: false 46 | ) 47 | |> Ash.Changeset.manage_relationship(:product, %{id: changeset.data.id}, type: :append) 48 | |> Ash.Changeset.manage_relationship(:option_values, combination, type: :append) 49 | |> Ash.Changeset.manage_relationship(:prices, prices, 50 | on_no_match: {:create, :create_for_variant}, 51 | on_match: :ignore 52 | ) 53 | # TODO add error handling and transaction logic. check for_create validation before committing changeset 54 | |> Ash.create!() 55 | end) 56 | 57 | changeset 58 | end 59 | 60 | # gets all the possible value combinations 61 | defp cartesian_product(list) do 62 | Enum.reduce(list, [[]], fn option, acc -> 63 | for combination <- acc, value <- option.option_values, do: [value | combination] 64 | end) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /tololo/core/lib/products/variant_option.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.Products.VariantOption do 2 | @moduledoc """ 3 | Join resource for the variants and options many_to_many relationship. 4 | """ 5 | use Ash.Resource, 6 | domain: TololoCore.Products, 7 | data_layer: AshPostgres.DataLayer 8 | 9 | postgres do 10 | table "variant_option" 11 | repo Tololo.Repo 12 | 13 | references do 14 | reference :variant, on_delete: :delete 15 | reference :option_value, on_delete: :delete 16 | end 17 | end 18 | 19 | actions do 20 | defaults [:read, :destroy, create: :*, update: :*] 21 | end 22 | 23 | relationships do 24 | belongs_to :variant, TololoCore.Products.Variant do 25 | primary_key? true 26 | allow_nil? false 27 | public? true 28 | end 29 | 30 | belongs_to :option_value, TololoCore.Products.OptionValue do 31 | primary_key? true 32 | allow_nil? false 33 | public? true 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /tololo/core/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoCore.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :tololo_core, 7 | version: "0.1.0", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | consolidate_protocols: Mix.env() != :dev, 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:ash_money, "~> 0.1"}, 26 | {:igniter, "~> 0.5", only: [:dev, :test]}, 27 | # {:dep_from_hexpm, "~> 0.3.0"}, 28 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 29 | {:ash_authentication, "~> 4.1"}, 30 | {:ash_authentication_phoenix, "~> 2.0"}, 31 | {:ash_graphql, "~> 1.7.3"}, 32 | {:ash_phoenix, "~> 2.1.14"}, 33 | {:ash_postgres, "~> 2.0"}, 34 | {:ash, "~> 3.0"}, 35 | {:ash_admin, "~> 0.12.6"}, 36 | {:ex_cldr, "~> 2.0"}, 37 | {:ex_cldr_numbers, "~> 2.33"}, 38 | {:ex_cldr_currencies, "~> 2.16"}, 39 | {:ex_cldr_dates_times, "~> 2.20"}, 40 | {:ex_cldr_calendars, "~> 1.26"}, 41 | {:ex_cldr_lists, "~> 2.11"}, 42 | {:ex_cldr_messages, "~> 1.0"}, 43 | {:ex_cldr_units, "~> 3.17"}, 44 | {:gettext, "~> 0.26"}, 45 | {:picosat_elixir, "~> 0.2.0"}, 46 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false} 47 | ] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /tololo/core/test/core_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreTest do 2 | use ExUnit.Case 3 | doctest Core 4 | 5 | test "greets the world" do 6 | assert Core.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /tololo/core/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tololo/extensions/kafka/lib/driver.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Kafka.Driver do 2 | @moduledoc """ 3 | Default driver implementation for Kafka. 4 | """ 5 | 6 | @behaviour TololoCore.Kafka 7 | 8 | @spec produce(String.t(), String.t(), term()) :: :ok | {:error, term()} 9 | def produce(topic, message, opts \\ []) do 10 | KafkaEx.produce(%KafkaEx.Protocol.Produce.Request{ 11 | topic: topic, 12 | partition: 0, 13 | required_acks: 1, 14 | messages: [%KafkaEx.Protocol.Produce.Message{value: message}] 15 | }) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /tololo/extensions/kafka/lib/extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Kafka do 2 | @moduledoc false 3 | use Supervisor 4 | use TololoCore.Extension 5 | 6 | @impl true 7 | def child_spec(init_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, [init_arg]}, 11 | type: :supervisor, 12 | restart: :permanent 13 | } 14 | end 15 | 16 | def start_link(_init_arg) do 17 | Application.put_env(:tololo, :kafka_driver, Tololo.Extensions.Kafka.Driver) 18 | Supervisor.start_link(__MODULE__, nil, name: __MODULE__) 19 | KafkaEx.create_worker(:kafka_ex) 20 | end 21 | 22 | @impl true 23 | def init(_) do 24 | children = [ 25 | {Tololo.Extensions.Kafka.Supervisor, max_restarts: 10, max_seconds: 60} 26 | ] 27 | 28 | Supervisor.init(children, strategy: :one_for_all) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tololo/extensions/kafka/lib/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Kafka.Supervisor do 2 | @moduledoc """ 3 | Wrapper needed to manually pass the KafkaEx supervisor to the main app's supervision tree. 4 | """ 5 | def start_link(opts) do 6 | KafkaEx.Supervisor.start_link(opts[:max_restarts], opts[:max_seconds]) 7 | end 8 | 9 | def child_spec(opts) do 10 | %{ 11 | id: __MODULE__, 12 | start: {__MODULE__, :start_link, [opts]}, 13 | type: :supervisor, 14 | restart: :permanent, 15 | } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /tololo/extensions/kafka/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Kafka.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :tololo_extension_kafka, 7 | version: "0.1.0", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:kafka_ex, "~> 0.11", runtime: false}, 23 | {:tololo_core, 24 | path: 25 | Path.join(["..", "..", "core"]) 26 | |> Path.expand()} 27 | ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /tololo/extensions/kafka/test/kafka/handler.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.KafkaTest do 2 | use ExUnit.Case 3 | doctest TololoExtensionKafka 4 | 5 | # test "greets the world" do 6 | # assert TololoExtensionTelegramBot.hello() == :world 7 | # end 8 | end 9 | -------------------------------------------------------------------------------- /tololo/extensions/kafka/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tololo/extensions/prometheus/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | tololo_extension_telegram_bot-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /tololo/extensions/prometheus/lib/extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Prometheus do 2 | @moduledoc false 3 | use Supervisor 4 | use TololoCore.Extension 5 | 6 | @impl true 7 | def child_spec(init_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, [init_arg]}, 11 | type: :supervisor, 12 | restart: :permanent 13 | } 14 | end 15 | 16 | def start_link(_init_arg) do 17 | Supervisor.start_link(__MODULE__, nil, name: __MODULE__) 18 | end 19 | 20 | @impl true 21 | def init(_) do 22 | children = [ 23 | Tololo.Extensions.Prometheus.PromEx 24 | ] 25 | 26 | Supervisor.init(children, strategy: :one_for_all) 27 | end 28 | 29 | @impl true 30 | def endpoint() do 31 | quote do 32 | plug PromEx.Plug, prom_ex_module: Tololo.Extensions.Prometheus.PromEx 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /tololo/extensions/prometheus/lib/promex.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Prometheus.PromEx do 2 | @moduledoc """ 3 | PromEx module, handles integration with Prometheus. 4 | """ 5 | 6 | use PromEx, otp_app: :tololo 7 | 8 | alias PromEx.Plugins 9 | 10 | @impl true 11 | def plugins do 12 | [ 13 | # PromEx built in plugins 14 | Plugins.Application, 15 | Plugins.Beam, 16 | {Plugins.Phoenix, router: TololoWeb.Router, endpoint: TololoWeb.Endpoint}, 17 | Plugins.Ecto, 18 | Plugins.PhoenixLiveView, 19 | Tololo.Extensions.Prometheus.PromExPlugin 20 | ] 21 | end 22 | 23 | @impl true 24 | def dashboard_assigns do 25 | [ 26 | datasource_id: "prometheus-tololo", 27 | default_selected_interval: "30s" 28 | ] 29 | end 30 | 31 | @impl true 32 | def dashboards do 33 | [ 34 | # PromEx built in Grafana dashboards 35 | {:prom_ex, "application.json"}, 36 | {:prom_ex, "beam.json"}, 37 | {:prom_ex, "phoenix.json"}, 38 | {:prom_ex, "ecto.json"}, 39 | {:prom_ex, "phoenix_live_view.json"}, 40 | 41 | {:tololo_extension_prometheus, "/dashboards/deliveries.json"} 42 | ] 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /tololo/extensions/prometheus/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.Prometheus.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :tololo_extension_prometheus, 7 | version: "0.1.0", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:phoenix, "~> 1.7.18"}, 23 | {:prom_ex, "~> 1.11.0"}, 24 | {:tololo_core, 25 | path: 26 | Path.join(["..", "..", "core"]) 27 | |> Path.expand()} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tololo/extensions/prometheus/test/prometheus/handler.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.PrometheusTest do 2 | use ExUnit.Case 3 | doctest TololoExtensionPrometheusBot 4 | 5 | # test "greets the world" do 6 | # assert TololoExtensionTelegramBot.hello() == :world 7 | # end 8 | end 9 | -------------------------------------------------------------------------------- /tololo/extensions/prometheus/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | tololo_extension_telegram_bot-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/config/dev.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Telegram Bot 2 | import Config 3 | 4 | config :telegex, token: System.get_env("TELEGRAM_BOT_TOKEN") || "" 5 | config :telegex, caller_adapter: Finch 6 | config :telegex, hook_adapter: Bandit 7 | 8 | # Note: webhook_url must be a full URL, such as https://your.domain.com/telegram, where updates_hook path is fixed. 9 | config :tololo, Tololo.Extensions.TelegramBot.Handler, 10 | webhook_url: System.get_env("TELEGRAM_WEBHOOK") || "" 11 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/config/prod.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Telegram Bot 2 | import Config 3 | 4 | config :telegex, token: System.get_env("TELEGRAM_BOT_TOKEN") || "" 5 | config :telegex, caller_adapter: Finch 6 | config :telegex, hook_adapter: Bandit 7 | 8 | # Note: webhook_url must be a full URL, such as https://your.domain.com/telegram, where updates_hook path is fixed. 9 | config :tololo, Tololo.Extensions.TelegramBot.Handler, 10 | webhook_url: System.get_env("TELEGRAM_WEBHOOK") || "" 11 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/config/test.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Telegram Bot 2 | import Config 3 | 4 | config :telegex, token: System.get_env("TELEGRAM_BOT_TOKEN") || "" 5 | config :telegex, caller_adapter: Finch 6 | config :telegex, hook_adapter: Bandit 7 | 8 | # Note: webhook_url must be a full URL, such as https://your.domain.com/telegram, where updates_hook path is fixed. 9 | config :tololo, Tololo.Extensions.TelegramBot.Handler, 10 | webhook_url: System.get_env("TELEGRAM_WEBHOOK") || "" 11 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/ash/users.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.Ash.Users do 2 | @moduledoc """ 3 | Domain that contains resources related to the delivery system. 4 | """ 5 | use Ash.Domain, 6 | otp_app: :tololo, 7 | extensions: [AshGraphql.Domain, AshAdmin.Domain], 8 | validate_config_inclusion?: false 9 | 10 | admin do 11 | show?(true) 12 | show_resources(Tololo.Extensions.TelegramBot.Ash.User) 13 | end 14 | 15 | resources do 16 | resource Tololo.Extensions.TelegramBot.Ash.User 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.Controller do 2 | # TODO: Implement proper docs 3 | @moduledoc false 4 | 5 | require Logger 6 | 7 | use Phoenix.Controller 8 | 9 | alias Tololo.Extensions.TelegramBot.Handler 10 | 11 | def update(conn, params) do 12 | params = atomize_keys(params) 13 | Logger.debug(params) 14 | 15 | update = Telegex.Helper.typedmap(params, Telegex.Type.Update) 16 | 17 | Logger.debug(update) 18 | 19 | try do 20 | Handler.on_update(update) 21 | rescue 22 | e -> 23 | Handler.on_failure(update, {e, __STACKTRACE__}) 24 | end 25 | |> case do 26 | {:done, %{payload: payload}} -> 27 | json(conn, payload) 28 | 29 | _ -> 30 | json(conn, %{}) 31 | end 32 | end 33 | 34 | defp atomize_keys(nil), do: nil 35 | 36 | defp atomize_keys(%{__struct__: _} = struct) do 37 | struct 38 | end 39 | 40 | defp atomize_keys(%{} = map) do 41 | map 42 | |> Enum.map(fn {k, v} -> {String.to_atom(k), atomize_keys(v)} end) 43 | |> Enum.into(%{}) 44 | end 45 | 46 | defp atomize_keys([head | rest]) do 47 | [atomize_keys(head) | atomize_keys(rest)] 48 | end 49 | 50 | defp atomize_keys(not_a_map) do 51 | not_a_map 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot do 2 | @moduledoc false 3 | alias Tololo.Extensions.TelegramBot 4 | use TololoCore.Extension 5 | 6 | use Supervisor 7 | 8 | @impl true 9 | def child_spec(init_arg) do 10 | %{ 11 | id: __MODULE__, 12 | start: {__MODULE__, :start_link, [init_arg]}, 13 | type: :supervisor, 14 | restart: :permanent 15 | } 16 | end 17 | 18 | def start_link(_init_arg) do 19 | Supervisor.start_link(__MODULE__, nil, name: __MODULE__) 20 | end 21 | 22 | @impl true 23 | def init(_) do 24 | TelegramBot.Handler.on_boot() 25 | 26 | children = [ 27 | {TelegramBot.Notifier, nil} 28 | ] 29 | 30 | Supervisor.init(children, strategy: :one_for_all) 31 | end 32 | 33 | @impl true 34 | def routes() do 35 | quote do 36 | pipeline :telegram_bot_api do 37 | plug :accepts, ["json"] 38 | end 39 | 40 | scope "/", Tololo.Extensions.TelegramBot do 41 | pipe_through :telegram_bot_api 42 | post "/telegram", Controller, :update 43 | end 44 | end 45 | end 46 | 47 | @impl true 48 | def ash_domains(), do: [Tololo.Extensions.TelegramBot.Ash.Users] 49 | end 50 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.Cldr do 2 | @moduledoc """ 3 | Config for CLDR. 4 | """ 5 | use Cldr, TololoCore.CldrConfig.common(Tololo.Extensions.TelegramBot.Gettext) 6 | end 7 | 8 | defmodule Tololo.Extensions.TelegramBot.Gettext.Interpolation do 9 | @moduledoc """ 10 | Define an interpolation module for ICU messages 11 | """ 12 | use Cldr.Gettext.Interpolation, cldr_backend: Tololo.Extensions.TelegramBot.Cldr 13 | end 14 | 15 | defmodule Tololo.Extensions.TelegramBot.Gettext do 16 | @moduledoc """ 17 | A module providing Internationalization with a gettext-based API. 18 | 19 | By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations 20 | that you can use in your application. To use this Gettext backend module, 21 | call `use Gettext` and pass it as an option: 22 | 23 | use Gettext, backend: Tololo.Extensions.TelegramBot.Gettext 24 | 25 | # Simple translation 26 | gettext("Here is the string to translate") 27 | 28 | # Plural translation 29 | ngettext("Here is the string to translate", 30 | "Here are the strings to translate", 31 | 3) 32 | 33 | # Domain-based translation 34 | dgettext("errors", "Here is the error message to translate") 35 | 36 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 37 | """ 38 | use Gettext.Backend, 39 | otp_app: :tololo_extension_telegram_bot, 40 | interpolation: Tololo.Extensions.TelegramBot.Gettext.Interpolation 41 | end 42 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/kafka/ash_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.Kafka.AshNotifier do 2 | use Ash.Notifier 3 | alias Ash.Notifier.Notification 4 | 5 | def notify(%Notification{ 6 | resource: resource, 7 | action: %{type: :create} 8 | }) do 9 | produce( 10 | resource, 11 | %{event: "user_request"} 12 | ) 13 | end 14 | 15 | def notify(%Notification{ 16 | resource: resource, 17 | action: %{name: :add_deliveries} 18 | }) do 19 | produce( 20 | resource, 21 | %{event: "user_add_deliveries"} 22 | ) 23 | end 24 | 25 | def notify(_notif), do: nil 26 | 27 | defp produce(resource, data), 28 | do: TololoCore.Kafka.produce("tololo-telegram", data |> Map.put(:resource, resource)) 29 | end 30 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/message.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.Message do 2 | @moduledoc """ 3 | Functions that help build messages in MarkdownV2 for Telegex. 4 | """ 5 | 6 | alias Telegex.Type.{ReplyKeyboardMarkup} 7 | 8 | def send_message(chat_id, text), 9 | do: %{ 10 | method: "sendMessage", 11 | chat_id: chat_id, 12 | text: escape_text(text), 13 | parse_mode: "MarkdownV2" 14 | } 15 | 16 | def send_message_with_keyboard(chat_id, text, buttons), 17 | do: 18 | send_message(chat_id, text) 19 | |> Map.merge(%{ 20 | reply_markup: %ReplyKeyboardMarkup{keyboard: [buttons], one_time_keyboard: true}, 21 | disable_web_page_preview: true 22 | }) 23 | 24 | def escape_text(text), 25 | do: 26 | text 27 | |> String.replace(".", "\\.") 28 | |> String.replace("-", "\\-") 29 | |> String.replace("!", "\\!") 30 | |> String.replace("_", "\\_") 31 | # |> String.replace("*", "\\*") 32 | |> String.replace("[", "\\[") 33 | |> String.replace("]", "\\]") 34 | |> String.replace("(", "\\(") 35 | |> String.replace(")", "\\)") 36 | end 37 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/telegram/chain_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.ChainContext do 2 | @moduledoc false 3 | 4 | use Telegex.Chain.Context 5 | 6 | defcontext([ 7 | {:chat_id, integer}, 8 | {:user_id, integer}, 9 | {:chat_title, String.t()}, 10 | {:user_resource, Tololo.Extensions.TelegramBot.Ash.User.t()} 11 | ]) 12 | end 13 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/telegram/chain_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.ChainHandler do 2 | @moduledoc false 3 | alias Tololo.Extensions.TelegramBot 4 | use Telegex.Chain.Handler 5 | 6 | pipeline([ 7 | TelegramBot.Auth, 8 | TelegramBot.ReceiveLocation, 9 | TelegramBot.SetToken, 10 | TelegramBot.SetTokenCallback, 11 | Tololo.Extensions.TelegramBot.Done 12 | ]) 13 | end 14 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/telegram/chains/list_deliveries.ex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/telegram/chains/receive_location.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.ReceiveLocation do 2 | @moduledoc false 3 | alias Tololo.Extensions.TelegramBot 4 | alias TololoCore.Deliveries 5 | 6 | use Gettext, backend: Tololo.Extensions.TelegramBot.Gettext 7 | use Telegex.Chain, :edited_message 8 | 9 | @actor TololoCore.Deliveries.Actors.private() 10 | 11 | @impl true 12 | def match?(%{chat: %{type: "private"}, location: _location}, _context), do: true 13 | @impl true 14 | def match?(_, _), do: false 15 | 16 | @impl true 17 | def handle( 18 | %{from: %{id: user_id}, location: %{latitude: lat, longitude: lng}}, 19 | %{user_resource: %{deliveries: deliveries}} = context 20 | ) do 21 | cond do 22 | deliveries == [] -> 23 | {:done, 24 | %{ 25 | context 26 | | payload: 27 | TelegramBot.Message.send_message( 28 | user_id, 29 | gettext("No active deliveries found. Please start one by using `/new {token}` or stop sharing location.") 30 | ) 31 | }} 32 | 33 | Enum.all?(deliveries, fn delivery -> 34 | Kernel.match?( 35 | {:ok, _}, 36 | Deliveries.Delivery.update_location(delivery, lat, lng, actor: @actor) 37 | ) 38 | end) -> 39 | {:ok, context} 40 | 41 | true -> 42 | {:done, 43 | %{ 44 | context 45 | | payload: 46 | TelegramBot.Message.send_message( 47 | user_id, 48 | gettext("Error trying to update location.") 49 | ) 50 | }} 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/lib/telegram/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.Handler do 2 | # TODO: Implement proper docs 3 | # TODO: Implement telegram bot commands 4 | @moduledoc false 5 | 6 | alias Tololo.Extensions.TelegramBot 7 | 8 | require Logger 9 | 10 | def on_boot do 11 | # read some parameters from your env config 12 | env_config = Application.get_env(:tololo, __MODULE__) 13 | 14 | # delete the webhook and set it again 15 | # set the webhook (url is required) 16 | try do 17 | {:ok, true} = Telegex.delete_webhook() 18 | {:ok, true} = Telegex.set_webhook(env_config[:webhook_url]) 19 | Logger.info("Telegram Webhook initialized") 20 | rescue 21 | _ -> Logger.info("Telegram Webhook not set") 22 | end 23 | end 24 | 25 | def on_update(update) do 26 | TelegramBot.ChainHandler.call(update, %TelegramBot.ChainContext{bot: Telegex.Instance.bot()}) 27 | end 28 | 29 | def on_failure(update, e) do 30 | Logger.error("Uncaught Error: #{inspect(update_id: update.update_id, error: e)}") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBot.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :tololo_extension_telegram_bot, 7 | version: "0.1.0", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:telegex, "~> 1.8.0"}, 23 | {:finch, "~> 0.13"}, 24 | {:multipart, "~> 0.4.0"}, 25 | {:remote_ip, "~> 1.2"}, 26 | {:ash_authentication, "~> 4.1"}, 27 | {:ash_authentication_phoenix, "~> 2.0"}, 28 | {:ash_graphql, "~> 1.7.3"}, 29 | {:ash_phoenix, "~> 2.1.14"}, 30 | {:ash_postgres, "~> 2.0"}, 31 | {:ash, "~> 3.0"}, 32 | {:ash_admin, "~> 0.12.6"}, 33 | {:phoenix, "~> 1.7.18"}, 34 | {:tololo_core, 35 | path: 36 | Path.join(["..", "..", "core"]) 37 | |> Path.expand()}, 38 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false} 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/test/telegram/handler.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Extensions.TelegramBotTest do 2 | use ExUnit.Case 3 | doctest TololoExtensionTelegramBot 4 | 5 | # test "greets the world" do 6 | # assert TololoExtensionTelegramBot.hello() == :world 7 | # end 8 | end 9 | -------------------------------------------------------------------------------- /tololo/extensions/telegram_bot/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tololo/fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for tololo on 2025-02-18T09:06:46-03:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'tololo' 7 | primary_region = 'scl' 8 | kill_signal = 'SIGTERM' 9 | 10 | [build] 11 | 12 | [deploy] 13 | release_command = '/app/bin/migrate' 14 | 15 | [env] 16 | PHX_HOST = 'tololo.fly.dev' 17 | PORT = '8080' 18 | 19 | [http_service] 20 | internal_port = 8080 21 | force_https = true 22 | auto_stop_machines = 'stop' 23 | auto_start_machines = true 24 | min_machines_running = 0 25 | processes = ['app'] 26 | 27 | [http_service.concurrency] 28 | type = 'connections' 29 | hard_limit = 1000 30 | soft_limit = 1000 31 | 32 | [[vm]] 33 | memory = '1gb' 34 | cpu_kind = 'shared' 35 | cpus = 1 36 | -------------------------------------------------------------------------------- /tololo/lib/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Cldr do 2 | @moduledoc """ 3 | Config for CLDR. 4 | """ 5 | use Cldr, TololoCore.CldrConfig.common(Tololo.Gettext) 6 | end 7 | 8 | defmodule Tololo.Gettext.Interpolation do 9 | @moduledoc """ 10 | Define an interpolation module for ICU messages 11 | """ 12 | use Cldr.Gettext.Interpolation, cldr_backend: Tololo.Cldr 13 | end 14 | 15 | defmodule Tololo.Gettext do 16 | @moduledoc """ 17 | A module providing Internationalization with a gettext-based API. 18 | 19 | By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations 20 | that you can use in your application. To use this Gettext backend module, 21 | call `use Gettext` and pass it as an option: 22 | 23 | use Gettext, backend: Tololo.Gettext 24 | 25 | # Simple translation 26 | gettext("Here is the string to translate") 27 | 28 | # Plural translation 29 | ngettext("Here is the string to translate", 30 | "Here are the strings to translate", 31 | 3) 32 | 33 | # Domain-based translation 34 | dgettext("errors", "Here is the error message to translate") 35 | 36 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 37 | """ 38 | use Gettext.Backend, otp_app: :tololo, interpolation: Tololo.Gettext.Interpolation 39 | end 40 | -------------------------------------------------------------------------------- /tololo/lib/mix/generate_delivery_env.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Generate.Delivery.Env do 2 | use Mix.Task 3 | 4 | @moduledoc """ 5 | A custom Mix task that initializes a delivery then generates a Bruno environment file for it. 6 | 7 | ## Usage 8 | 9 | mix generate.delivery.env (optional dir) 10 | """ 11 | 12 | @template """ 13 | vars { 14 | gql_host: 127.0.0.1 15 | gql_port: 4000 16 | <%= for {key, value} <- extra_vars do %> 17 | <%= key %>: <%= value %> 18 | <% end %> 19 | } 20 | """ 21 | @impl Mix.Task 22 | def run(args) do 23 | dir = List.first(args) || "../collections/environments" 24 | Mix.Task.run("app.start") 25 | 26 | %{id: id, public_auth_key: public_auth_key, private_auth_key: private_auth_key} = 27 | TololoCore.Deliveries.Delivery.empty!(authorize?: false) 28 | 29 | output = 30 | EEx.eval_string(@template, 31 | extra_vars: %{ 32 | id: id, 33 | public_auth_key: public_auth_key, 34 | private_auth_key: private_auth_key, 35 | admin_auth_key: System.get_env("ADMIN_API_KEY") 36 | } 37 | ) 38 | 39 | File.write!(dir <> "/generated_env.bru", output) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /tololo/lib/tololo.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo do 2 | @moduledoc """ 3 | Tololo keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /tololo/lib/tololo/accounts.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Accounts do 2 | @moduledoc """ 3 | Domain that contains resources related to the general account system. 4 | """ 5 | use Ash.Domain, 6 | extensions: [AshAdmin.Domain], 7 | otp_app: :tololo 8 | 9 | admin do 10 | show?(true) 11 | show_resources(Tololo.Accounts.User) 12 | end 13 | 14 | resources do 15 | resource Tololo.Accounts.Token 16 | resource Tololo.Accounts.User 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tololo/lib/tololo/accounts/user/senders/send_magic_link_email.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Accounts.User.Senders.SendMagicLinkEmail do 2 | @moduledoc """ 3 | Sends a magic link email 4 | """ 5 | @sender_email Application.compile_env(:tololo, :from_email) 6 | @subject Application.compile_env(:tololo, :magic_link_subject) 7 | 8 | use AshAuthentication.Sender 9 | use TololoWeb, :verified_routes 10 | 11 | import Swoosh.Email 12 | alias Tololo.Mailer 13 | 14 | @impl true 15 | def send(user_or_email, token, _) do 16 | # if you get a user, its for a user that already exists. 17 | # if you get an email, then the user does not yet exist. 18 | 19 | email = 20 | case user_or_email do 21 | %{email: email} -> email 22 | email -> email 23 | end 24 | 25 | new() 26 | |> from(@sender_email) 27 | |> to(to_string(email)) 28 | |> subject(@subject) 29 | |> html_body(TololoWeb.EmailTemplates.send_magic_link(%{token: token, email: email})) 30 | |> Mailer.deliver!() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /tololo/lib/tololo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | if System.get_env("ECTO_IPV6") do 11 | :httpc.set_option(:ipfamily, :inet6fb4) 12 | end 13 | 14 | :ok = OpentelemetryBandit.setup() 15 | :ok = OpentelemetryPhoenix.setup(adapter: :bandit) 16 | 17 | :ok = 18 | Tololo.Repo.config() 19 | |> Keyword.fetch!(:telemetry_prefix) 20 | |> OpentelemetryEcto.setup() 21 | 22 | extensions = Application.get_env(:tololo, :extensions, []) 23 | 24 | children = 25 | [ 26 | TololoWeb.Telemetry, 27 | Tololo.Repo, 28 | {DNSCluster, query: Application.get_env(:tololo, :dns_cluster_query) || :ignore}, 29 | {Phoenix.PubSub, name: Tololo.PubSub}, 30 | # Start the Finch HTTP client for sending emails 31 | {Finch, name: Tololo.Finch}, 32 | TololoWeb.Endpoint, 33 | {AshAuthentication.Supervisor, [otp_app: :tololo]}, 34 | TololoCore.Deliveries.StaleCleaner, 35 | {Task, fn -> load_config() end} 36 | ] ++ extensions 37 | 38 | Tololo.GeocodingStore.init() 39 | 40 | # See https://hexdocs.pm/elixir/Supervisor.html 41 | # for other strategies and supported options 42 | opts = [strategy: :one_for_one, name: Tololo.Supervisor] 43 | Supervisor.start_link(children, opts) 44 | end 45 | 46 | # Tell Phoenix to update the endpoint configuration 47 | # whenever the application is updated. 48 | @impl true 49 | def config_change(changed, _new, removed) do 50 | TololoWeb.Endpoint.config_change(changed, removed) 51 | :ok 52 | end 53 | 54 | def load_config do 55 | %{latitude: lat, longitude: lng, name: name} = 56 | case TololoCore.Brands.Branch.read() do 57 | {:ok, branch_config} -> branch_config 58 | _ -> %{latitude: 0, longitude: 0, name: "A business"} 59 | end 60 | 61 | Application.put_env(:tololo, :from_location, {lat, lng}) 62 | Application.put_env(:tololo, :business_name, name) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /tololo/lib/tololo/geocoding.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Geocoding do 2 | @moduledoc """ 3 | Module for geocoding. It uses the Nominatim API as a fallback. It can be changed through config. 4 | """ 5 | def query(query_string) do 6 | case Tololo.GeocodingStore.get_cached(query_string) do 7 | {:ok, result} -> result 8 | _ -> fetch_from_provider(query_string) 9 | end 10 | end 11 | 12 | defp fetch_from_provider(query_string) do 13 | endpoint = 14 | Application.get_env( 15 | :tololo, 16 | :geocoding_endpoint, 17 | "https://nominatim.openstreetmap.org/search" 18 | ) 19 | 20 | token = Application.get_env(:tololo, :geocoding_token, nil) 21 | 22 | # add token if it exists, for compatible API services such as LocationIQ 23 | params = [q: query_string, format: "json"] ++ if token, do: [token: token], else: [] 24 | 25 | # return only first result 26 | [result | _tail] = 27 | Application.get_env(:tololo, :req_impl, Req).get!(endpoint, params: params).body 28 | 29 | Tololo.GeocodingStore.save_in_cache(query_string, result) 30 | result 31 | end 32 | end 33 | 34 | defmodule Tololo.GeocodingStore do 35 | @moduledoc """ 36 | ETS cache for the Tololo.Geocoding module. Should be migrated to Postgres in the future. 37 | """ 38 | @table_name :geocoding_cache 39 | 40 | def init do 41 | :ets.new(@table_name, [:set, :public, :named_table]) 42 | end 43 | 44 | def save_in_cache(query, result) do 45 | :ets.insert(@table_name, {query, result}) 46 | end 47 | 48 | def get_cached(query) do 49 | case :ets.lookup(@table_name, query) do 50 | [{_query, result}] -> {:ok, result} 51 | [] -> :error 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /tololo/lib/tololo/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Mailer do 2 | use Swoosh.Mailer, otp_app: :tololo 3 | end 4 | -------------------------------------------------------------------------------- /tololo/lib/tololo/release.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Release do 2 | @moduledoc """ 3 | Used for executing DB release tasks when run in production without Mix 4 | installed. 5 | """ 6 | @app :tololo 7 | 8 | def migrate do 9 | load_app() 10 | 11 | for repo <- repos() do 12 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) 13 | end 14 | end 15 | 16 | def load_example_store do 17 | load_app() 18 | 19 | Path.join([:code.priv_dir(@app), "/repo"], "example_seeds.exs") 20 | |> Code.require_file() 21 | end 22 | 23 | def reset do 24 | load_app() 25 | 26 | for repo <- repos() do 27 | {:ok, _, _} = 28 | Ecto.Migrator.with_repo(repo, fn repo -> 29 | Ecto.Adapters.SQL.query!(repo, "DROP SCHEMA public CASCADE") 30 | Ecto.Adapters.SQL.query!(repo, "CREATE SCHEMA public") 31 | end) 32 | end 33 | 34 | migrate() 35 | load_example_store() 36 | end 37 | 38 | def rollback(repo, version) do 39 | load_app() 40 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 41 | end 42 | 43 | defp repos do 44 | Application.fetch_env!(@app, :ecto_repos) 45 | end 46 | 47 | defp load_app do 48 | Application.load(@app) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /tololo/lib/tololo/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo do 2 | use AshPostgres.Repo, 3 | otp_app: :tololo 4 | 5 | def installed_extensions do 6 | # Add extensions here, and the migration generator will install them. 7 | ["ash-functions", "citext", AshMoney.AshPostgresExtension] 8 | end 9 | 10 | # Don't open unnecessary transactions 11 | # will default to `false` in 4.0 12 | def prefer_transaction? do 13 | false 14 | end 15 | 16 | def min_pg_version do 17 | %Version{major: 16, minor: 5, patch: 0} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /tololo/lib/tololo/request_stub.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.RequestStub do 2 | @moduledoc false 3 | # https://medium.com/@dimakoua/mocking-http-requests-in-elixir-a-practical-guide-7b177dfd9725 4 | use Agent 5 | 6 | def start_link(_opts) do 7 | Agent.start_link(fn -> %{} end, name: __MODULE__) 8 | end 9 | 10 | def get!(url, _opts) do 11 | Agent.get(__MODULE__, &Map.get(&1, url, %{body: []})) 12 | end 13 | 14 | def set_response(url, response) do 15 | Agent.update(__MODULE__, &Map.put(&1, url, response)) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /tololo/lib/tololo/secrets.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Secrets do 2 | @moduledoc """ 3 | Secrets for AshAuthentication. 4 | """ 5 | use AshAuthentication.Secret 6 | 7 | def secret_for([:authentication, :tokens, :signing_secret], Tololo.Accounts.User, _opts) do 8 | Application.fetch_env(:tololo, :token_signing_secret) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/auth_overrides.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.AuthOverrides do 2 | use AshAuthentication.Phoenix.Overrides 3 | @moduledoc false 4 | 5 | # configure your UI overrides here 6 | 7 | # First argument to `override` is the component name you are overriding. 8 | # The body contains any number of configurations you wish to override 9 | # Below are some examples 10 | 11 | # For a complete reference, see https://hexdocs.pm/ash_authentication_phoenix/ui-overrides.html 12 | 13 | # override AshAuthentication.Phoenix.Components.Banner do 14 | # set :image_url, "https://media.giphy.com/media/g7GKcSzwQfugw/giphy.gif" 15 | # set :text_class, "bg-red-500" 16 | # end 17 | 18 | # override AshAuthentication.Phoenix.Components.SignIn do 19 | # set :show_banner false 20 | # end 21 | end 22 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use TololoWeb, :controller` and 9 | `use TololoWeb, :live_view`. 10 | """ 11 | use TololoWeb, :html 12 | 13 | embed_templates "layouts/*" 14 | end 15 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | v{Application.spec(:tololo, :vsn)} 9 | 10 | 11 | 12 | 13 | GitHub 14 | 15 | 16 | 17 | 18 | 19 | 20 | <.flash_group flash={@flash} /> 21 | {@inner_content} 22 | 23 | 24 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title default="Tololo" suffix=" · Phoenix Framework"> 8 | {assigns[:page_title]} 9 | 10 | 11 | 13 | {assigns[:head_includes]} 14 | 15 | 16 | {@inner_content} 17 | 18 | 19 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/controllers/auth_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.AuthController do 2 | use TololoWeb, :controller 3 | use AshAuthentication.Phoenix.Controller 4 | use Gettext, backend: Tololo.Gettext 5 | 6 | def success(conn, activity, user, _token) do 7 | return_to = get_session(conn, :return_to) || ~p"/" 8 | 9 | message = 10 | case activity do 11 | {:confirm_new_user, :confirm} -> gettext("Your email address has now been confirmed") 12 | {:password, :reset} -> gettext("Your password has successfully been reset") 13 | _ -> gettext("You are now signed in") 14 | end 15 | 16 | conn 17 | |> delete_session(:return_to) 18 | |> store_in_session(user) 19 | # If your resource has a different name, update the assign name here (i.e :current_admin) 20 | |> assign(:current_user, user) 21 | |> put_flash(:info, message) 22 | |> redirect(to: return_to) 23 | end 24 | 25 | def failure(conn, activity, reason) do 26 | message = 27 | case {activity, reason} do 28 | {_, 29 | %AshAuthentication.Errors.AuthenticationFailed{ 30 | caused_by: %Ash.Error.Forbidden{ 31 | errors: [%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}] 32 | } 33 | }} -> 34 | gettext(""" 35 | You have already signed in another way, but have not confirmed your account. 36 | You can confirm your account using the link we sent to you, or by resetting your password. 37 | """) 38 | 39 | _ -> 40 | gettext("Incorrect email or password") 41 | end 42 | 43 | conn 44 | |> put_flash(:error, message) 45 | |> redirect(to: ~p"/sign-in") 46 | end 47 | 48 | def sign_out(conn, _params) do 49 | return_to = get_session(conn, :return_to) || ~p"/" 50 | 51 | conn 52 | |> clear_session() 53 | |> put_flash(:info, gettext("You are now signed out")) 54 | |> redirect(to: return_to) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use TololoWeb, :html 8 | 9 | def render(_template, %{reason: %TololoWeb.NotFoundError{message: message}}), do: message 10 | 11 | # The default is to render a plain text page based on 12 | # the template name. For example, "404.html" becomes 13 | # "Not Found". 14 | def render(template, _assigns) do 15 | Phoenix.Controller.status_message_from_template(template) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.PageController do 2 | use TololoWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use TololoWeb, :html 8 | 9 | embed_templates "page_html/*" 10 | end 11 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/deliveries/delivery_auth_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.Deliveries.DeliveryAuthPlug do 2 | @moduledoc """ 3 | Sets actor of the request, based on the authorization token passed through headers. If there's no authorization header, it returns the conn unmodified. If there's an invalid token, it crashes. 4 | """ 5 | @behaviour Plug 6 | import Plug.Conn 7 | alias AshAuthentication.Plug.Helpers 8 | 9 | @impl true 10 | def init(opts), do: opts 11 | 12 | @impl true 13 | def call(%{assigns: %{current_user: %{admin?: true}}} = conn, _opts) do 14 | conn 15 | |> assign(:actor, generate_actor(:admin)) 16 | |> Helpers.set_actor(:actor) 17 | end 18 | 19 | @impl true 20 | def call(conn, _opts) do 21 | with false <- Map.has_key?(conn.assigns, :actor), 22 | ["Bearer " <> token] <- get_req_header(conn, "authorization"), 23 | {actor, resource} <- get_token_data(token) do 24 | conn 25 | |> assign(:resource, resource) 26 | |> assign(:actor, actor) 27 | |> Helpers.set_actor(:actor) 28 | else 29 | _ -> conn 30 | end 31 | end 32 | 33 | @spec get_token_data(String.t()) :: 34 | {%{access_level: atom()}, TololoCore.Deliveries.Delivery.t()} 35 | def get_token_data(token) do 36 | cond do 37 | admin?(token) -> 38 | # admin token isn't related to a resource, so it returns nil 39 | {generate_actor(:admin), nil} 40 | 41 | [resource] = TololoCore.Deliveries.Delivery.get_via_token!(token, authorize?: false) -> 42 | {generate_actor(token, resource), resource} 43 | end 44 | end 45 | 46 | @spec generate_actor(atom()) :: %{access_level: atom()} 47 | defp generate_actor(:admin), do: TololoCore.Deliveries.Actors.admin() 48 | 49 | @spec generate_actor(atom(), TololoCore.Deliveries.Delivery.t()) :: %{access_level: atom()} 50 | defp generate_actor(token, %{public_auth_key: public_key, private_auth_key: private_key}) do 51 | cond do 52 | token == public_key -> TololoCore.Deliveries.Actors.public() 53 | token == private_key -> TololoCore.Deliveries.Actors.private() 54 | end 55 | end 56 | 57 | @spec admin?(String.t()) :: boolean() 58 | defp admin?(token), do: token == System.get_env("ADMIN_API_KEY") 59 | end 60 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/email_templates.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.EmailTemplates do 2 | @moduledoc """ 3 | HTML templates for emails. 4 | """ 5 | use TololoWeb, :html 6 | 7 | def send_magic_link(assigns) do 8 | ~H""" 9 | 10 | Hello, {@email}! Click 11 | 12 | this link 13 | 14 | to sign in. 15 | 16 | If the link doesn't work, manually enter: 17 | 18 | {url(~p"/auth/user/magic_link/?token=#{@token}")} 19 | 20 | """ 21 | |> Phoenix.HTML.Safe.to_iodata() 22 | |> IO.iodata_to_binary() 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :tololo 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_tololo_key", 10 | signing_salt: "Trgfjfw6", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket "/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug Plug.Static, 23 | at: "/", 24 | from: :tololo, 25 | gzip: false, 26 | only: TololoWeb.static_paths() 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :tololo 35 | end 36 | 37 | plug Phoenix.LiveDashboard.RequestLogger, 38 | param_key: "request_logger", 39 | cookie_key: "request_logger" 40 | 41 | plug Plug.RequestId 42 | 43 | use TololoCore.Extension, :endpoint 44 | 45 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 46 | 47 | plug Plug.Parsers, 48 | parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser], 49 | pass: ["*/*"], 50 | json_decoder: Phoenix.json_library() 51 | 52 | plug Plug.MethodOverride 53 | plug Plug.Head 54 | plug Plug.Session, @session_options 55 | plug TololoWeb.Router 56 | end 57 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/graphql_schema.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.GraphqlSchema do 2 | @moduledoc """ 3 | Schema for GraphQL endpoints. 4 | """ 5 | use Absinthe.Schema 6 | 7 | use AshGraphql, 8 | domains: [TololoCore.Deliveries, TololoCore.Carts, TololoCore.Brands, TololoCore.Products], 9 | generate_sdl_file: "priv/schema.graphql" 10 | 11 | import_types Absinthe.Plug.Types 12 | 13 | query do 14 | # Custom Absinthe queries can be placed here 15 | @desc """ 16 | Hello! This is a sample query to verify that AshGraphql has been set up correctly. 17 | Remove me once you have a query of your own! 18 | """ 19 | field :say_hello, :string do 20 | resolve fn _, _, _ -> 21 | {:ok, "Hello from AshGraphql!"} 22 | end 23 | end 24 | end 25 | 26 | object :money do 27 | field(:amount, non_null(:decimal)) 28 | field(:currency, non_null(:string)) 29 | end 30 | 31 | input_object :money_input do 32 | field(:amount, non_null(:decimal)) 33 | field(:currency, non_null(:string)) 34 | end 35 | 36 | mutation do 37 | # Custom Absinthe mutations can be placed here 38 | end 39 | 40 | subscription do 41 | # Custom Absinthe subscriptions can be placed here 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/live/map_live.html.heex: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | {gettext("Order history")}: 12 | 13 | <%= for change <- @resource.state_history do %> 14 | 15 | {date_to_string(change.inserted_at)} - {change.comment} 16 | 17 | <% end %> 18 | 19 | 20 | -------------------------------------------------------------------------------- /tololo/lib/tololo_web/live_user_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.LiveUserAuth do 2 | @moduledoc """ 3 | Helpers for authenticating users in LiveViews. 4 | """ 5 | 6 | use Gettext, backend: Tololo.Gettext 7 | import Phoenix.Component 8 | use TololoWeb, :verified_routes 9 | 10 | def on_mount( 11 | :live_user_admin_required, 12 | _params, 13 | _session, 14 | %{assigns: %{current_user: %{admin?: true}}} = socket 15 | ) do 16 | {:cont, socket} 17 | end 18 | 19 | def on_mount( 20 | :live_user_admin_required, 21 | _params, 22 | _session, 23 | %{assigns: %{current_user: %{admin?: false}}} = _socket 24 | ), 25 | do: raise(TololoWeb.ForbiddenError, gettext("You don't have access to this page")) 26 | 27 | def on_mount( 28 | :live_user_admin_required, 29 | _params, 30 | _session, 31 | socket 32 | ) do 33 | {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")} 34 | end 35 | 36 | def on_mount(:live_user_optional, _params, _session, socket) do 37 | if socket.assigns[:current_user] do 38 | {:cont, socket} 39 | else 40 | {:cont, assign(socket, :current_user, nil)} 41 | end 42 | end 43 | 44 | def on_mount(:live_user_required, _params, _session, socket) do 45 | if socket.assigns[:current_user] do 46 | {:cont, socket} 47 | else 48 | {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")} 49 | end 50 | end 51 | 52 | def on_mount(:live_no_user, _params, _session, socket) do 53 | if socket.assigns[:current_user] do 54 | {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/")} 55 | else 56 | {:cont, assign(socket, :current_user, nil)} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250120201203_deliveries_extensions_1.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesExtensions1 do 2 | @moduledoc """ 3 | Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | execute("ALTER FUNCTION ash_raise_error(jsonb) STABLE;") 12 | execute("ALTER FUNCTION ash_raise_error(jsonb, ANYCOMPATIBLE) STABLE") 13 | 14 | execute(""" 15 | CREATE OR REPLACE FUNCTION uuid_generate_v7() 16 | RETURNS UUID 17 | AS $$ 18 | DECLARE 19 | timestamp TIMESTAMPTZ; 20 | microseconds INT; 21 | BEGIN 22 | timestamp = clock_timestamp(); 23 | microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT; 24 | 25 | RETURN encode( 26 | set_byte( 27 | set_byte( 28 | overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6 29 | ), 30 | 6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int 31 | ), 32 | 7, microseconds::bit(8)::int 33 | ), 34 | 'hex')::UUID; 35 | END 36 | $$ 37 | LANGUAGE PLPGSQL 38 | SET search_path = '' 39 | VOLATILE; 40 | """) 41 | 42 | execute(""" 43 | CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid) 44 | RETURNS TIMESTAMP WITHOUT TIME ZONE 45 | AS $$ 46 | SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000); 47 | $$ 48 | LANGUAGE SQL 49 | SET search_path = '' 50 | IMMUTABLE PARALLEL SAFE STRICT; 51 | """) 52 | end 53 | 54 | def down do 55 | # Uncomment this if you actually want to uninstall the extensions 56 | # when this migration is rolled back: 57 | execute("ALTER FUNCTION ash_raise_error(jsonb) VOLATILE;") 58 | execute("ALTER FUNCTION ash_raise_error(jsonb, ANYCOMPATIBLE) VOLATILE") 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250121163709_add_authentication_resources_extensions_1.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.AddAuthenticationResourcesExtensions1 do 2 | @moduledoc """ 3 | Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | execute("CREATE EXTENSION IF NOT EXISTS \"citext\"") 12 | end 13 | 14 | def down do 15 | # Uncomment this if you actually want to uninstall the extensions 16 | # when this migration is rolled back: 17 | # execute("DROP EXTENSION IF EXISTS \"citext\"") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250121163711_add_authentication_resources.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.AddAuthenticationResources do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create table(:users, primary_key: false) do 12 | add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true 13 | end 14 | 15 | create table(:tokens, primary_key: false) do 16 | add :created_at, :utc_datetime_usec, 17 | null: false, 18 | default: fragment("(now() AT TIME ZONE 'utc')") 19 | 20 | add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true 21 | add :jti, :text, null: false, primary_key: true 22 | add :subject, :text, null: false 23 | add :expires_at, :utc_datetime, null: false 24 | add :purpose, :text, null: false 25 | add :extra_data, :map 26 | 27 | add :inserted_at, :utc_datetime_usec, 28 | null: false, 29 | default: fragment("(now() AT TIME ZONE 'utc')") 30 | 31 | add :updated_at, :utc_datetime_usec, 32 | null: false, 33 | default: fragment("(now() AT TIME ZONE 'utc')") 34 | end 35 | end 36 | 37 | def down do 38 | drop table(:tokens) 39 | 40 | drop table(:users) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250121180624_deliveries_indexes.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesIndexes do 2 | @moduledoc """ 3 | Create indexes for public and private auth keys. 4 | """ 5 | 6 | use Ecto.Migration 7 | 8 | def change do 9 | create index(:deliveries, [:public_auth_key]) 10 | create index(:deliveries, [:private_auth_key]) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250127131349_deliveries_current_pos.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesCurrentPos do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:deliveries) do 12 | modify :state, :text, default: "Init" 13 | add :current_latitude, :float 14 | add :current_longitude, :float 15 | end 16 | end 17 | 18 | def down do 19 | alter table(:deliveries) do 20 | remove :current_longitude 21 | remove :current_latitude 22 | modify :state, :text, default: nil 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250127135249_deliveries_auth_keys.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesAuthKeys do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:deliveries) do 12 | modify :public_auth_key, :uuid, default: fragment("uuid_generate_v7()") 13 | modify :private_auth_key, :uuid, default: fragment("uuid_generate_v7()") 14 | end 15 | end 16 | 17 | def down do 18 | alter table(:deliveries) do 19 | modify :private_auth_key, :uuid, default: nil 20 | modify :public_auth_key, :uuid, default: nil 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250131120145_deliveries_display_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesDisplayId do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:deliveries) do 12 | add :display_id, :text 13 | end 14 | 15 | create index(:deliveries, [:display_id]) 16 | end 17 | 18 | def down do 19 | alter table(:deliveries) do 20 | remove :display_id 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250204002021_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.Users do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create table(:telegram_bot, primary_key: false) do 12 | add :id, :text, null: false, primary_key: true 13 | add :status, :text, null: false 14 | add :deliveries_id, {:array, :uuid} 15 | 16 | add :inserted_at, :utc_datetime_usec, 17 | null: false, 18 | default: fragment("(now() AT TIME ZONE 'utc')") 19 | 20 | add :updated_at, :utc_datetime_usec, 21 | null: false, 22 | default: fragment("(now() AT TIME ZONE 'utc')") 23 | end 24 | end 25 | 26 | def down do 27 | drop table(:telegram_bot) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250205234835_deliveries_on_delete.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesOnDelete do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | drop constraint(:delivery_state_changes, "delivery_state_changes_delivery_id_fkey") 12 | 13 | alter table(:delivery_state_changes) do 14 | modify :delivery_id, 15 | references(:deliveries, 16 | column: :id, 17 | name: "delivery_state_changes_delivery_id_fkey", 18 | type: :uuid, 19 | prefix: "public", 20 | on_delete: :delete_all 21 | ) 22 | end 23 | end 24 | 25 | def down do 26 | drop constraint(:delivery_state_changes, "delivery_state_changes_delivery_id_fkey") 27 | 28 | alter table(:delivery_state_changes) do 29 | modify :delivery_id, 30 | references(:deliveries, 31 | column: :id, 32 | name: "delivery_state_changes_delivery_id_fkey", 33 | type: :uuid, 34 | prefix: "public" 35 | ) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250219172630_deliveries_nullable.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DeliveriesNullable do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:deliveries) do 12 | modify :to_address, :text, null: false 13 | modify :to_longitude, :float, null: false 14 | modify :to_latitude, :float, null: false 15 | end 16 | end 17 | 18 | def down do 19 | alter table(:deliveries) do 20 | modify :to_latitude, :float, null: true 21 | modify :to_longitude, :float, null: true 22 | modify :to_address, :text, null: true 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250224180533_magic_link.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.MagicLink do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:users) do 12 | add :email, :citext, null: false 13 | end 14 | 15 | create unique_index(:users, [:email], name: "users_unique_email_index") 16 | end 17 | 18 | def down do 19 | drop_if_exists unique_index(:users, [:email], name: "users_unique_email_index") 20 | 21 | alter table(:users) do 22 | remove :email 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250225022131_user_admin_field.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.UserAdminField do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:users) do 12 | add :admin?, :boolean, default: false 13 | end 14 | end 15 | 16 | def down do 17 | alter table(:users) do 18 | remove :admin? 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250303150336_option_identity.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.OptionIdentity do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create unique_index(:options, [:name, :product_id], name: "options_unique_option_index") 12 | end 13 | 14 | def down do 15 | drop_if_exists unique_index(:options, [:name, :product_id], 16 | name: "options_unique_option_index" 17 | ) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250303170125_value_identity.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.ValueIdentity do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create unique_index(:option_values, [:value, :option_id], 12 | name: "option_values_unique_value_index" 13 | ) 14 | end 15 | 16 | def down do 17 | drop_if_exists unique_index(:option_values, [:value, :option_id], 18 | name: "option_values_unique_value_index" 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250303185557_variant_update.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.VariantUpdate do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:variants) do 12 | add :option_value_ids, {:array, :text} 13 | end 14 | 15 | create unique_index(:variants, [:option_value_ids, :product_id], 16 | name: "variants_unique_variant_index" 17 | ) 18 | end 19 | 20 | def down do 21 | drop_if_exists unique_index(:variants, [:option_value_ids, :product_id], 22 | name: "variants_unique_variant_index" 23 | ) 24 | 25 | alter table(:variants) do 26 | remove :option_value_ids 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250304123947_prices.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.Prices do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create table(:prices, primary_key: false) do 12 | add :id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true 13 | add :money, :money_with_currency, null: false 14 | 15 | add :inserted_at, :utc_datetime_usec, 16 | null: false, 17 | default: fragment("(now() AT TIME ZONE 'utc')") 18 | 19 | add :updated_at, :utc_datetime_usec, 20 | null: false, 21 | default: fragment("(now() AT TIME ZONE 'utc')") 22 | 23 | add :product_id, 24 | references(:products, 25 | column: :id, 26 | name: "prices_product_id_fkey", 27 | type: :uuid, 28 | prefix: "public", 29 | on_delete: :delete_all 30 | ) 31 | 32 | add :variant_id, 33 | references(:products, 34 | column: :id, 35 | name: "prices_variant_id_fkey", 36 | type: :uuid, 37 | prefix: "public", 38 | on_delete: :delete_all 39 | ) 40 | end 41 | end 42 | 43 | def down do 44 | drop constraint(:prices, "prices_product_id_fkey") 45 | 46 | drop constraint(:prices, "prices_variant_id_fkey") 47 | 48 | drop table(:prices) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250304135428_price_identities.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.PriceIdentities do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | drop constraint(:prices, "prices_variant_id_fkey") 12 | 13 | alter table(:prices) do 14 | modify :variant_id, 15 | references(:variants, 16 | column: :id, 17 | name: "prices_variant_id_fkey", 18 | type: :uuid, 19 | prefix: "public", 20 | on_delete: :delete_all 21 | ) 22 | end 23 | 24 | create unique_index(:prices, ["((money).currency_code)", :product_id], 25 | name: "prices_unique_product_index" 26 | ) 27 | 28 | create unique_index(:prices, ["((money).currency_code)", :variant_id], 29 | name: "prices_unique_variant_index" 30 | ) 31 | end 32 | 33 | def down do 34 | drop_if_exists unique_index(:prices, ["((money).currency_code)", :variant_id], 35 | name: "prices_unique_variant_index" 36 | ) 37 | 38 | drop_if_exists unique_index(:prices, ["((money).currency_code)", :product_id], 39 | name: "prices_unique_product_index" 40 | ) 41 | 42 | drop constraint(:prices, "prices_variant_id_fkey") 43 | 44 | alter table(:prices) do 45 | modify :variant_id, 46 | references(:products, 47 | column: :id, 48 | name: "prices_variant_id_fkey", 49 | type: :uuid, 50 | prefix: "public", 51 | on_delete: :delete_all 52 | ) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250307094233_discount_rules.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.DiscountRules do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:variants) do 12 | add :discount_rules, {:array, :map}, default: [] 13 | end 14 | 15 | alter table(:products) do 16 | add :discount_rules, {:array, :map}, default: [] 17 | end 18 | end 19 | 20 | def down do 21 | alter table(:products) do 22 | remove :discount_rules 23 | end 24 | 25 | alter table(:variants) do 26 | remove :discount_rules 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250310183831_brand.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.Brand do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create table(:brands, primary_key: false) do 12 | add :id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true 13 | add :name, :text, null: false 14 | add :hq_url, :text 15 | 16 | add :inserted_at, :utc_datetime_usec, 17 | null: false, 18 | default: fragment("(now() AT TIME ZONE 'utc')") 19 | 20 | add :updated_at, :utc_datetime_usec, 21 | null: false, 22 | default: fragment("(now() AT TIME ZONE 'utc')") 23 | end 24 | 25 | create table(:branches, primary_key: false) do 26 | add :id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true 27 | add :address, :text, null: false 28 | add :latitude, :float, null: false 29 | add :longitude, :float, null: false 30 | 31 | add :inserted_at, :utc_datetime_usec, 32 | null: false, 33 | default: fragment("(now() AT TIME ZONE 'utc')") 34 | 35 | add :updated_at, :utc_datetime_usec, 36 | null: false, 37 | default: fragment("(now() AT TIME ZONE 'utc')") 38 | 39 | add :brand_id, 40 | references(:brands, 41 | column: :id, 42 | name: "branches_brand_id_fkey", 43 | type: :uuid, 44 | prefix: "public" 45 | ) 46 | end 47 | end 48 | 49 | def down do 50 | drop constraint(:branches, "branches_brand_id_fkey") 51 | 52 | drop table(:branches) 53 | 54 | drop table(:brands) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /tololo/priv/repo/migrations/20250310194854_branch_name.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.Repo.Migrations.BranchName do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:branches) do 12 | add :name, :text, null: false 13 | end 14 | end 15 | 16 | def down do 17 | alter table(:branches) do 18 | remove :name 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tololo/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Tololo.Repo.insert!(%Tololo.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/brands/20250310183831.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"uuid_generate_v7()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "name", 21 | "type": "text" 22 | }, 23 | { 24 | "allow_nil?": true, 25 | "default": "nil", 26 | "generated?": false, 27 | "primary_key?": false, 28 | "references": null, 29 | "size": null, 30 | "source": "hq_url", 31 | "type": "text" 32 | }, 33 | { 34 | "allow_nil?": false, 35 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 36 | "generated?": false, 37 | "primary_key?": false, 38 | "references": null, 39 | "size": null, 40 | "source": "inserted_at", 41 | "type": "utc_datetime_usec" 42 | }, 43 | { 44 | "allow_nil?": false, 45 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 46 | "generated?": false, 47 | "primary_key?": false, 48 | "references": null, 49 | "size": null, 50 | "source": "updated_at", 51 | "type": "utc_datetime_usec" 52 | } 53 | ], 54 | "base_filter": null, 55 | "check_constraints": [], 56 | "custom_indexes": [], 57 | "custom_statements": [], 58 | "has_create_action": true, 59 | "hash": "0B24FBD23BFF3F08B324FD4667F0437E738D29E173C80F9CE6B8536C362F15EF", 60 | "identities": [], 61 | "multitenancy": { 62 | "attribute": null, 63 | "global": null, 64 | "strategy": null 65 | }, 66 | "repo": "Elixir.Tololo.Repo", 67 | "schema": null, 68 | "table": "brands" 69 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/carts/20250304164127.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"uuid_generate_v7()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": true, 15 | "default": "\"active\"", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "status", 21 | "type": "text" 22 | }, 23 | { 24 | "allow_nil?": true, 25 | "default": "\"CLP\"", 26 | "generated?": false, 27 | "primary_key?": false, 28 | "references": null, 29 | "size": null, 30 | "source": "currency", 31 | "type": "text" 32 | }, 33 | { 34 | "allow_nil?": false, 35 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 36 | "generated?": false, 37 | "primary_key?": false, 38 | "references": null, 39 | "size": null, 40 | "source": "inserted_at", 41 | "type": "utc_datetime_usec" 42 | }, 43 | { 44 | "allow_nil?": false, 45 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 46 | "generated?": false, 47 | "primary_key?": false, 48 | "references": null, 49 | "size": null, 50 | "source": "updated_at", 51 | "type": "utc_datetime_usec" 52 | } 53 | ], 54 | "base_filter": null, 55 | "check_constraints": [], 56 | "custom_indexes": [], 57 | "custom_statements": [], 58 | "has_create_action": true, 59 | "hash": "9BE9014469CF153B9B7BC96C3458000BEBDF38F2ECC06D7DBA81FA838B9DC027", 60 | "identities": [], 61 | "multitenancy": { 62 | "attribute": null, 63 | "global": null, 64 | "strategy": null 65 | }, 66 | "repo": "Elixir.Tololo.Repo", 67 | "schema": null, 68 | "table": "carts" 69 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "ash_functions_version": 5, 3 | "installed": [ 4 | "ash-functions", 5 | "citext", 6 | "ash_money_v5" 7 | ] 8 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/product_option/20250228122847.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "nil", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": { 9 | "deferrable": false, 10 | "destination_attribute": "id", 11 | "destination_attribute_default": null, 12 | "destination_attribute_generated": null, 13 | "index?": false, 14 | "match_type": null, 15 | "match_with": null, 16 | "multitenancy": { 17 | "attribute": null, 18 | "global": null, 19 | "strategy": null 20 | }, 21 | "name": "product_option_product_id_fkey", 22 | "on_delete": null, 23 | "on_update": null, 24 | "primary_key?": true, 25 | "schema": "public", 26 | "table": "products" 27 | }, 28 | "size": null, 29 | "source": "product_id", 30 | "type": "uuid" 31 | }, 32 | { 33 | "allow_nil?": false, 34 | "default": "nil", 35 | "generated?": false, 36 | "primary_key?": true, 37 | "references": { 38 | "deferrable": false, 39 | "destination_attribute": "id", 40 | "destination_attribute_default": null, 41 | "destination_attribute_generated": null, 42 | "index?": false, 43 | "match_type": null, 44 | "match_with": null, 45 | "multitenancy": { 46 | "attribute": null, 47 | "global": null, 48 | "strategy": null 49 | }, 50 | "name": "product_option_option_id_fkey", 51 | "on_delete": null, 52 | "on_update": null, 53 | "primary_key?": true, 54 | "schema": "public", 55 | "table": "options" 56 | }, 57 | "size": null, 58 | "source": "option_id", 59 | "type": "uuid" 60 | } 61 | ], 62 | "base_filter": null, 63 | "check_constraints": [], 64 | "custom_indexes": [], 65 | "custom_statements": [], 66 | "has_create_action": true, 67 | "hash": "72C1267958CAC37AAE4E1895BB6D890D198E3CFD25448219B6A87B9B538C9E92", 68 | "identities": [], 69 | "multitenancy": { 70 | "attribute": null, 71 | "global": null, 72 | "strategy": null 73 | }, 74 | "repo": "Elixir.Tololo.Repo", 75 | "schema": null, 76 | "table": "product_option" 77 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/product_option/20250307145708.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "nil", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": { 9 | "deferrable": false, 10 | "destination_attribute": "id", 11 | "destination_attribute_default": null, 12 | "destination_attribute_generated": null, 13 | "index?": false, 14 | "match_type": null, 15 | "match_with": null, 16 | "multitenancy": { 17 | "attribute": null, 18 | "global": null, 19 | "strategy": null 20 | }, 21 | "name": "product_option_product_id_fkey", 22 | "on_delete": "delete", 23 | "on_update": null, 24 | "primary_key?": true, 25 | "schema": "public", 26 | "table": "products" 27 | }, 28 | "size": null, 29 | "source": "product_id", 30 | "type": "uuid" 31 | }, 32 | { 33 | "allow_nil?": false, 34 | "default": "nil", 35 | "generated?": false, 36 | "primary_key?": true, 37 | "references": { 38 | "deferrable": false, 39 | "destination_attribute": "id", 40 | "destination_attribute_default": null, 41 | "destination_attribute_generated": null, 42 | "index?": false, 43 | "match_type": null, 44 | "match_with": null, 45 | "multitenancy": { 46 | "attribute": null, 47 | "global": null, 48 | "strategy": null 49 | }, 50 | "name": "product_option_option_id_fkey", 51 | "on_delete": "delete", 52 | "on_update": null, 53 | "primary_key?": true, 54 | "schema": "public", 55 | "table": "options" 56 | }, 57 | "size": null, 58 | "source": "option_id", 59 | "type": "uuid" 60 | } 61 | ], 62 | "base_filter": null, 63 | "check_constraints": [], 64 | "custom_indexes": [], 65 | "custom_statements": [], 66 | "has_create_action": true, 67 | "hash": "485859D0150ECF41C57E6EC3CBC0A15E83F26525B32AC64B0CDAC0D240D527A6", 68 | "identities": [], 69 | "multitenancy": { 70 | "attribute": null, 71 | "global": null, 72 | "strategy": null 73 | }, 74 | "repo": "Elixir.Tololo.Repo", 75 | "schema": null, 76 | "table": "product_option" 77 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/representatives/20250107183939.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"gen_random_uuid()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "name", 21 | "type": "text" 22 | } 23 | ], 24 | "base_filter": null, 25 | "check_constraints": [], 26 | "custom_indexes": [], 27 | "custom_statements": [], 28 | "has_create_action": true, 29 | "hash": "137BB8FB87082549BD8B44F64C4DF40314077A1B66B35DADB15344BA3B3A98F1", 30 | "identities": [], 31 | "multitenancy": { 32 | "attribute": null, 33 | "global": null, 34 | "strategy": null 35 | }, 36 | "repo": "Elixir.Tololo.Repo", 37 | "schema": null, 38 | "table": "representatives" 39 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/telegram_bot/20250204002021.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "nil", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "text" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "status", 21 | "type": "text" 22 | }, 23 | { 24 | "allow_nil?": true, 25 | "default": "nil", 26 | "generated?": false, 27 | "primary_key?": false, 28 | "references": null, 29 | "size": null, 30 | "source": "deliveries_id", 31 | "type": [ 32 | "array", 33 | "uuid" 34 | ] 35 | }, 36 | { 37 | "allow_nil?": false, 38 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 39 | "generated?": false, 40 | "primary_key?": false, 41 | "references": null, 42 | "size": null, 43 | "source": "inserted_at", 44 | "type": "utc_datetime_usec" 45 | }, 46 | { 47 | "allow_nil?": false, 48 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 49 | "generated?": false, 50 | "primary_key?": false, 51 | "references": null, 52 | "size": null, 53 | "source": "updated_at", 54 | "type": "utc_datetime_usec" 55 | } 56 | ], 57 | "base_filter": null, 58 | "check_constraints": [], 59 | "custom_indexes": [], 60 | "custom_statements": [], 61 | "has_create_action": false, 62 | "hash": "E4351624E2AD77635FE39954BB1D78211DCAE37C7328DF32940B5F25780B806A", 63 | "identities": [], 64 | "multitenancy": { 65 | "attribute": null, 66 | "global": null, 67 | "strategy": null 68 | }, 69 | "repo": "Elixir.Tololo.Repo", 70 | "schema": null, 71 | "table": "telegram_bot" 72 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/tickets/20250107183939.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"gen_random_uuid()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "subject", 21 | "type": "text" 22 | }, 23 | { 24 | "allow_nil?": false, 25 | "default": "\"open\"", 26 | "generated?": false, 27 | "primary_key?": false, 28 | "references": null, 29 | "size": null, 30 | "source": "status", 31 | "type": "text" 32 | }, 33 | { 34 | "allow_nil?": true, 35 | "default": "nil", 36 | "generated?": false, 37 | "primary_key?": false, 38 | "references": { 39 | "deferrable": false, 40 | "destination_attribute": "id", 41 | "destination_attribute_default": null, 42 | "destination_attribute_generated": null, 43 | "index?": false, 44 | "match_type": null, 45 | "match_with": null, 46 | "multitenancy": { 47 | "attribute": null, 48 | "global": null, 49 | "strategy": null 50 | }, 51 | "name": "tickets_representative_id_fkey", 52 | "on_delete": null, 53 | "on_update": null, 54 | "primary_key?": true, 55 | "schema": "public", 56 | "table": "representatives" 57 | }, 58 | "size": null, 59 | "source": "representative_id", 60 | "type": "uuid" 61 | } 62 | ], 63 | "base_filter": null, 64 | "check_constraints": [], 65 | "custom_indexes": [], 66 | "custom_statements": [], 67 | "has_create_action": true, 68 | "hash": "484C32AFF4D3AA00F68848C6F69CEE523D27DE39F0A7105081BDE956B035BD97", 69 | "identities": [], 70 | "multitenancy": { 71 | "attribute": null, 72 | "global": null, 73 | "strategy": null 74 | }, 75 | "repo": "Elixir.Tololo.Repo", 76 | "schema": null, 77 | "table": "tickets" 78 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/types/20250228122847.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"uuid_generate_v7()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "name", 21 | "type": "text" 22 | }, 23 | { 24 | "allow_nil?": true, 25 | "default": "nil", 26 | "generated?": false, 27 | "primary_key?": false, 28 | "references": null, 29 | "size": null, 30 | "source": "description", 31 | "type": "text" 32 | }, 33 | { 34 | "allow_nil?": true, 35 | "default": "nil", 36 | "generated?": false, 37 | "primary_key?": false, 38 | "references": null, 39 | "size": null, 40 | "source": "attributes", 41 | "type": [ 42 | "array", 43 | "map" 44 | ] 45 | }, 46 | { 47 | "allow_nil?": false, 48 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 49 | "generated?": false, 50 | "primary_key?": false, 51 | "references": null, 52 | "size": null, 53 | "source": "inserted_at", 54 | "type": "utc_datetime_usec" 55 | }, 56 | { 57 | "allow_nil?": false, 58 | "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 59 | "generated?": false, 60 | "primary_key?": false, 61 | "references": null, 62 | "size": null, 63 | "source": "updated_at", 64 | "type": "utc_datetime_usec" 65 | } 66 | ], 67 | "base_filter": null, 68 | "check_constraints": [], 69 | "custom_indexes": [], 70 | "custom_statements": [], 71 | "has_create_action": true, 72 | "hash": "BF3EEBA4D7F91DEDCC6AB5F61BA9A39F699E035C206EB9A8C986CFA18170D5CE", 73 | "identities": [], 74 | "multitenancy": { 75 | "attribute": null, 76 | "global": null, 77 | "strategy": null 78 | }, 79 | "repo": "Elixir.Tololo.Repo", 80 | "schema": null, 81 | "table": "types" 82 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/users/20250121163711.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"gen_random_uuid()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | } 13 | ], 14 | "base_filter": null, 15 | "check_constraints": [], 16 | "custom_indexes": [], 17 | "custom_statements": [], 18 | "has_create_action": false, 19 | "hash": "48FA47256A2B9186B3442268B4468D898920565A7824DBD666B67F66864248DC", 20 | "identities": [], 21 | "multitenancy": { 22 | "attribute": null, 23 | "global": null, 24 | "strategy": null 25 | }, 26 | "repo": "Elixir.Tololo.Repo", 27 | "schema": null, 28 | "table": "users" 29 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/users/20250224180533.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"gen_random_uuid()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "email", 21 | "type": "citext" 22 | } 23 | ], 24 | "base_filter": null, 25 | "check_constraints": [], 26 | "custom_indexes": [], 27 | "custom_statements": [], 28 | "has_create_action": true, 29 | "hash": "A6D46BAE1B5C1154B42ED271BD15C5563883EFDD2C13723EA2A97E2B8F2D62C3", 30 | "identities": [ 31 | { 32 | "all_tenants?": false, 33 | "base_filter": null, 34 | "index_name": "users_unique_email_index", 35 | "keys": [ 36 | { 37 | "type": "atom", 38 | "value": "email" 39 | } 40 | ], 41 | "name": "unique_email", 42 | "nils_distinct?": true, 43 | "where": null 44 | } 45 | ], 46 | "multitenancy": { 47 | "attribute": null, 48 | "global": null, 49 | "strategy": null 50 | }, 51 | "repo": "Elixir.Tololo.Repo", 52 | "schema": null, 53 | "table": "users" 54 | } -------------------------------------------------------------------------------- /tololo/priv/resource_snapshots/repo/users/20250225022131.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "allow_nil?": false, 5 | "default": "fragment(\"gen_random_uuid()\")", 6 | "generated?": false, 7 | "primary_key?": true, 8 | "references": null, 9 | "size": null, 10 | "source": "id", 11 | "type": "uuid" 12 | }, 13 | { 14 | "allow_nil?": false, 15 | "default": "nil", 16 | "generated?": false, 17 | "primary_key?": false, 18 | "references": null, 19 | "size": null, 20 | "source": "email", 21 | "type": "citext" 22 | }, 23 | { 24 | "allow_nil?": true, 25 | "default": "false", 26 | "generated?": false, 27 | "primary_key?": false, 28 | "references": null, 29 | "size": null, 30 | "source": "admin?", 31 | "type": "boolean" 32 | } 33 | ], 34 | "base_filter": null, 35 | "check_constraints": [], 36 | "custom_indexes": [], 37 | "custom_statements": [], 38 | "has_create_action": true, 39 | "hash": "8B7F01862D812C28FD8FC6BD5E7B7E9CF6813B4C23FB22249697065DBB301A47", 40 | "identities": [ 41 | { 42 | "all_tenants?": false, 43 | "base_filter": null, 44 | "index_name": "users_unique_email_index", 45 | "keys": [ 46 | { 47 | "type": "atom", 48 | "value": "email" 49 | } 50 | ], 51 | "name": "unique_email", 52 | "nils_distinct?": true, 53 | "where": null 54 | } 55 | ], 56 | "multitenancy": { 57 | "attribute": null, 58 | "global": null, 59 | "strategy": null 60 | }, 61 | "repo": "Elixir.Tololo.Repo", 62 | "schema": null, 63 | "table": "users" 64 | } -------------------------------------------------------------------------------- /tololo/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElixirCL/tololo/92e17dbf70b9f9440c9527c54084cdce543a56cc/tololo/priv/static/favicon.ico -------------------------------------------------------------------------------- /tololo/priv/static/images/map/flatware.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tololo/priv/static/images/map/home.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tololo/priv/static/images/map/motorcycle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tololo/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /tololo/rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # configure node for distributed erlang with IPV6 support 4 | export ERL_AFLAGS="-proto_dist inet6_tcp" 5 | export ECTO_IPV6="true" 6 | export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal" 7 | export RELEASE_DISTRIBUTION="name" 8 | export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}" 9 | 10 | # Uncomment to send crash dumps to stderr 11 | # This can be useful for debugging, but may log sensitive information 12 | # export ERL_CRASH_DUMP=/dev/stderr 13 | # export ERL_CRASH_DUMP_BYTES=4096 14 | -------------------------------------------------------------------------------- /tololo/rel/overlays/bin/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | exec ./tololo eval Tololo.Release.migrate 6 | -------------------------------------------------------------------------------- /tololo/rel/overlays/bin/migrate.bat: -------------------------------------------------------------------------------- 1 | call "%~dp0\tololo" eval Tololo.Release.migrate 2 | -------------------------------------------------------------------------------- /tololo/rel/overlays/bin/server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | PHX_SERVER=true exec ./tololo start 6 | -------------------------------------------------------------------------------- /tololo/rel/overlays/bin/server.bat: -------------------------------------------------------------------------------- 1 | set PHX_SERVER=true 2 | call "%~dp0\tololo" start 3 | -------------------------------------------------------------------------------- /tololo/test/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Tololo.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | You may define functions here to be used as helpers in 6 | your tests. 7 | Finally, if the test case interacts with the database, 8 | we enable the SQL sandbox, so changes done to the database 9 | are reverted at the end of every test. If you are using 10 | PostgreSQL, you can even run database tests asynchronously 11 | by setting `use Tololo.DataCase, async: true`, although 12 | this option is not recommended for other databases. 13 | """ 14 | use ExUnit.CaseTemplate 15 | 16 | using do 17 | quote do 18 | alias Tololo.Repo 19 | import Ecto 20 | import Ecto.Changeset 21 | import Ecto.Query 22 | import Tololo.DataCase 23 | end 24 | end 25 | 26 | setup tags do 27 | Tololo.DataCase.setup_sandbox(tags) 28 | :ok 29 | end 30 | 31 | @doc """ 32 | Sets up the sandbox based on the test tags. 33 | """ 34 | def setup_sandbox(tags) do 35 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Tololo.Repo, shared: not tags[:async]) 36 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 37 | end 38 | 39 | @doc """ 40 | A helper that transforms changeset errors into a map of messages. 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | """ 45 | def errors_on(changeset) do 46 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 47 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 48 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 49 | end) 50 | end) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /tololo/test/mix/generate_delivery_env_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Generate.Delivery.EnvTest do 2 | use Tololo.DataCase, async: false 3 | import ExUnit.CaptureIO 4 | 5 | setup do 6 | # create temp dir for test isolation 7 | tmp_dir = Path.join(System.tmp_dir!(), "delivery_env_test") 8 | File.mkdir_p!(tmp_dir) 9 | on_exit(fn -> File.rm_rf!(tmp_dir) end) 10 | 11 | {:ok, tmp_dir: tmp_dir} 12 | end 13 | 14 | test "generates .env files in specified directory", %{tmp_dir: tmp_dir} do 15 | output = 16 | capture_io(fn -> 17 | Mix.Tasks.Generate.Delivery.Env.run([tmp_dir]) 18 | end) 19 | 20 | assert File.exists?(Path.join(tmp_dir, "generated_env.bru")) 21 | assert output == "" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /tololo/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Tololo.Repo, :auto) 3 | -------------------------------------------------------------------------------- /tololo/test/tololo/carts/cart_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CartTest do 2 | use Tololo.DataCase, async: true 3 | 4 | alias TololoCore.Carts.Cart 5 | alias TololoCore.Products.{Product, Option} 6 | 7 | setup do 8 | product = 9 | Product.create!( 10 | %{ 11 | name: "Test Product", 12 | description: "Description", 13 | state: :enabled, 14 | sku: "TEST_SKU", 15 | discount_rules: [] 16 | }, 17 | authorize?: false 18 | ) 19 | |> Product.update_price!(%{amount: 1000, currency: :CLP}, authorize?: false) 20 | 21 | option = Product.get_or_create_option!(product, "Color", authorize?: false) 22 | Option.add_value!(option, "Red", authorize?: false) 23 | 24 | product = 25 | product 26 | |> Ash.load!(options: [:option_values]) 27 | |> Product.generate_variants!(authorize?: false) 28 | |> Ash.load!(variants: [:prices]) 29 | 30 | variant = hd(product.variants) 31 | 32 | {:ok, product: product, variant: variant} 33 | end 34 | 35 | describe "cart operations" do 36 | test "adds variant to cart", %{variant: variant} do 37 | cart = Cart.create!() 38 | %{cart_lines: [line]} = Cart.add_variant!(cart, variant.id, 2, "Handle with care") 39 | 40 | assert line.quantity == 2 41 | assert line.notes == "Handle with care" 42 | end 43 | 44 | test "performs checkout with delivery", %{variant: variant} do 45 | cart = Cart.create!() 46 | |> Cart.add_variant!(variant.id, 1, "") 47 | 48 | delivery_public_token = 49 | Cart.checkout_delivery!( 50 | cart.id, 51 | %{ 52 | to_name: "Nombre", 53 | to_address: "123", 54 | to_phone: "12345678", 55 | to_latitude: 40.7128, 56 | to_longitude: -74.0060, 57 | delivery_person: %{}, 58 | delivery_order: %{} 59 | } 60 | ) 61 | assert TololoCore.Deliveries.Delivery.get_via_token(delivery_public_token) 62 | end 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /tololo/test/tololo/geocoding_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Tololo.GeocodingTest do 2 | use ExUnit.Case, async: false 3 | import Tololo.RequestStub 4 | 5 | setup do 6 | start_supervised(Tololo.RequestStub) 7 | 8 | table_name = :geocoding_cache 9 | 10 | if :ets.whereis(table_name) == :undefined do 11 | Tololo.GeocodingStore.init() 12 | else 13 | :ets.delete_all_objects(table_name) 14 | end 15 | 16 | :ok 17 | end 18 | 19 | test "returns cached result for repeated queries" do 20 | set_response("https://nominatim.openstreetmap.org/search", %{body: [%{"test" => "data"}]}) 21 | 22 | assert Tololo.Geocoding.query("test") == %{"test" => "data"} 23 | 24 | # should return old cached data 25 | set_response("https://nominatim.openstreetmap.org/search", %{body: [%{"new" => "data"}]}) 26 | assert Tololo.Geocoding.query("test") == %{"test" => "data"} 27 | 28 | assert {:ok, Tololo.Geocoding.query("test")} == Tololo.GeocodingStore.get_cached("test") 29 | end 30 | 31 | test "handles API failures gracefully" do 32 | set_response("https://nominatim.openstreetmap.org/search", %{body: []}) 33 | 34 | assert_raise MatchError, fn -> 35 | Tololo.Geocoding.query("failure_test") 36 | end 37 | 38 | assert Tololo.GeocodingStore.get_cached("failure_test") == :error 39 | end 40 | 41 | test "uses configured endpoint and token" do 42 | # revert env change after test 43 | original_endpoint = Application.get_env(:tololo, :geocoding_endpoint) 44 | original_token = Application.get_env(:tololo, :geocoding_token) 45 | 46 | on_exit(fn -> 47 | Application.put_env(:tololo, :geocoding_endpoint, original_endpoint) 48 | Application.put_env(:tololo, :geocoding_token, original_token) 49 | end) 50 | 51 | Application.put_env(:tololo, :geocoding_endpoint, "https://custom.geo/api") 52 | Application.put_env(:tololo, :geocoding_token, "secret") 53 | 54 | set_response("https://custom.geo/api", %{body: [%{"custom" => "data"}]}) 55 | 56 | assert Tololo.Geocoding.query("custom_endpoint") == %{"custom" => "data"} 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | Such tests rely on `Phoenix.ConnTest` and also 6 | import other functionality to make it easier 7 | to build common data structures and query the data layer. 8 | Finally, if the test case interacts with the database, 9 | we enable the SQL sandbox, so changes done to the database 10 | are reverted at the end of every test. If you are using 11 | PostgreSQL, you can even run database tests asynchronously 12 | by setting `use TololoWeb.ConnCase, async: true`, although 13 | this option is not recommended for other databases. 14 | """ 15 | use ExUnit.CaseTemplate 16 | import Plug.Conn 17 | 18 | using do 19 | quote do 20 | # The default endpoint for testing 21 | @endpoint TololoWeb.Endpoint 22 | use TololoWeb, :verified_routes 23 | # Import conveniences for testing with connections 24 | import Plug.Conn 25 | import Phoenix.ConnTest 26 | import TololoWeb.ConnCase 27 | end 28 | end 29 | 30 | setup tags do 31 | Tololo.DataCase.setup_sandbox(tags) 32 | {:ok, conn: Phoenix.ConnTest.build_conn() |> Plug.Test.init_test_session(%{})} 33 | end 34 | 35 | def admin_session(conn) do 36 | user = 37 | Ash.create!(Tololo.Accounts.User, %{email: "test@email.com", admin?: true}, 38 | authorize?: false 39 | ) 40 | 41 | conn 42 | |> fetch_session 43 | |> put_session("user", AshAuthentication.user_to_subject(user)) 44 | |> put_session("tenant", nil) 45 | |> put_session("context", nil) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.ErrorHTMLTest do 2 | use TololoWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(TololoWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders custom error pages" do 12 | assert render_to_string(TololoWeb.ErrorHTML, "404", "html", 13 | reason: %TololoWeb.NotFoundError{message: "error message"} 14 | ) == "error message" 15 | end 16 | 17 | test "renders 500.html" do 18 | assert render_to_string(TololoWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.ErrorJSONTest do 2 | use TololoWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert TololoWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 | end 7 | 8 | test "renders 500" do 9 | assert TololoWeb.ErrorJSON.render("500.json", %{}) == 10 | %{errors: %{detail: "Internal Server Error"}} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.PageControllerTest do 2 | use TololoWeb.ConnCase 3 | 4 | def auth_conn(conn), 5 | do: 6 | conn 7 | |> Plug.Conn.put_req_header( 8 | "authorization", 9 | "Basic " <> Base.encode64("admin:#{System.get_env("ADMIN_API_KEY")}") 10 | ) 11 | 12 | test "GET /", %{conn: conn} do 13 | conn = get(conn, ~p"/") 14 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 15 | end 16 | 17 | test "GET /gql/playground", %{conn: conn} do 18 | conn = get(conn, ~p"/gql/playground") 19 | assert response(conn, 400) 20 | end 21 | 22 | test "GET /admin", %{conn: conn} do 23 | conn = 24 | conn 25 | |> auth_conn() 26 | |> get(~p"/admin") 27 | 28 | assert response(conn, 200) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/gettext_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GettextTest do 2 | @moduledoc """ 3 | Test that Getttext translations are correctly loaded for English and Spanish. 4 | """ 5 | alias TololoCore.Deliveries 6 | alias TololoCore.Deliveries.Transitions 7 | 8 | use Tololo.DataCase, async: true 9 | 10 | describe "delivery state messages" do 11 | test "that In_Preparation has correct message" do 12 | delivery = 13 | Deliveries.Delivery.empty!(authorize?: false) 14 | |> Deliveries.Delivery.update_state!(:In_Preparation, authorize?: false) 15 | 16 | %{state_history: [%{old_state: old_state, new_state: new_state}]} = 17 | Ash.load!(delivery, :state_history) 18 | 19 | # Default Message 20 | message = Transitions.message(old_state, new_state) 21 | assert message == "The order is processing" 22 | 23 | # English Message 24 | Gettext.with_locale("en", fn -> 25 | message = Transitions.message(old_state, new_state) 26 | assert message == "The order is processing" 27 | end) 28 | 29 | # Spanish Message 30 | Gettext.with_locale("es", fn -> 31 | message = Transitions.message(old_state, new_state) 32 | assert message == "La orden está en preparación" 33 | end) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/live/delivery_live/form_component_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.DeliveryLive.FormComponentTest do 2 | use TololoWeb.ConnCase 3 | import Phoenix.LiveViewTest 4 | 5 | alias TololoCore.Deliveries.Delivery 6 | 7 | @create_attrs %{ 8 | to_name: "valid", 9 | to_latitude: 1.23, 10 | to_longitude: 1.23, 11 | to_address: "Valparaíso, Chile" 12 | } 13 | @invalid_attrs %{ 14 | to_name: "invalid", 15 | to_latitude: nil, 16 | to_longitude: nil, 17 | to_address: nil 18 | } 19 | @update_attrs %{state: "In_Preparation"} 20 | 21 | setup %{conn: conn} do 22 | {:ok, conn: conn |> TololoWeb.ConnCase.admin_session()} 23 | end 24 | 25 | describe "Form Component" do 26 | setup do 27 | {:ok, delivery: Delivery.empty!(actor: TololoCore.Deliveries.Actors.admin())} 28 | end 29 | 30 | test "renders form", %{conn: conn} do 31 | {:ok, view, _html} = live(conn, ~p"/deliveries/new") 32 | 33 | assert render(view) =~ "New Delivery" 34 | assert view |> element("form") |> has_element?() 35 | assert view |> element("#delivery_to_name") |> render() 36 | end 37 | 38 | test "validates form inputs", %{conn: conn} do 39 | {:ok, view, _html} = live(conn, ~p"/deliveries/new") 40 | 41 | view 42 | |> form("#delivery-form", delivery: @invalid_attrs) 43 | |> render_change() 44 | 45 | assert render(view) =~ "is required" 46 | end 47 | 48 | test "handles valid create submission", %{conn: conn} do 49 | {:ok, view, _html} = live(conn, ~p"/deliveries/new") 50 | 51 | assert view 52 | |> form("#delivery-form") 53 | |> render_submit(delivery: @create_attrs) =~ "created successfully" 54 | end 55 | 56 | test "handles valid edit submission", %{conn: conn, delivery: delivery} do 57 | {:ok, view, _html} = live(conn, ~p"/deliveries/#{delivery.id}/edit") 58 | 59 | assert view 60 | |> form("#delivery-form") 61 | |> render_submit(delivery: @update_attrs) =~ "updated successfully" 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/live/delivery_live/index_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.DeliveryLive.IndexTest do 2 | use TololoWeb.ConnCase 3 | import Phoenix.LiveViewTest 4 | 5 | alias TololoCore.Deliveries.Delivery 6 | 7 | setup %{conn: conn} do 8 | {:ok, 9 | deliveries: 10 | Enum.map(0..5, fn _ -> Delivery.empty!(actor: TololoCore.Deliveries.Actors.admin()) end), 11 | conn: conn |> TololoWeb.ConnCase.admin_session()} 12 | end 13 | 14 | test "lists deliveries", %{conn: conn, deliveries: deliveries} do 15 | {:ok, _index_live, html} = live(conn, ~p"/deliveries") 16 | 17 | deliveries 18 | |> Enum.each(fn %{display_id: display_id} -> 19 | assert html =~ display_id 20 | end) 21 | end 22 | 23 | test "handles delete action", %{conn: conn, deliveries: [delivery | _]} do 24 | {:ok, index_live, _html} = live(conn, ~p"/deliveries") 25 | 26 | assert index_live 27 | |> element("#deliveries-#{delivery.id} a", "Delete") 28 | |> render_click() 29 | 30 | assert {:error, _} = 31 | Ash.get(Delivery, delivery.id, actor: TololoCore.Deliveries.Actors.admin()) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/live/delivery_live/session.ex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/live/delivery_live/show_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.DeliveryLive.ShowTest do 2 | use TololoWeb.ConnCase 3 | import Phoenix.LiveViewTest 4 | 5 | alias TololoCore.Deliveries.Delivery 6 | 7 | setup %{conn: conn} do 8 | {:ok, 9 | delivery: Delivery.empty!(actor: TololoCore.Deliveries.Actors.admin()), 10 | conn: conn |> TololoWeb.ConnCase.admin_session()} 11 | end 12 | 13 | def auth_conn(conn), 14 | do: 15 | conn 16 | |> Plug.Conn.put_req_header( 17 | "authorization", 18 | "Basic " <> Base.encode64("admin:#{System.get_env("ADMIN_API_KEY")}") 19 | ) 20 | 21 | test "displays delivery details", %{conn: conn, delivery: delivery} do 22 | conn = conn |> auth_conn 23 | {:ok, _show_live, html} = live(conn, ~p"/deliveries/#{delivery.id}") 24 | 25 | assert html =~ "Delivery #{delivery.display_id}" 26 | assert html =~ "Init" 27 | end 28 | 29 | test "redirects to edit when clicking edit button", %{conn: conn, delivery: delivery} do 30 | conn = conn |> auth_conn 31 | 32 | {:ok, show_live, _html} = live(conn, ~p"/deliveries/#{delivery.id}") 33 | 34 | assert show_live 35 | |> element("a", "Edit Delivery") 36 | |> render_click() =~ 37 | "Update delivery" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /tololo/test/tololo_web/live/map_live_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TololoWeb.MapLiveTest do 2 | use TololoWeb.ConnCase, async: false 3 | use Gettext, backend: Tololo.Gettext 4 | 5 | import Phoenix.LiveViewTest 6 | 7 | defp get_element_string(string), do: ~s(.leaflet-marker-icon[title="#{string}"]) 8 | 9 | describe "map" do 10 | test "raises error for missing token", %{conn: conn} do 11 | assert_raise TololoWeb.NotFoundError, gettext("Token not found"), fn -> 12 | live_isolated(conn, TololoWeb.MapLive) 13 | end 14 | end 15 | 16 | test "raises error for invalid token", %{conn: conn} do 17 | assert_raise TololoWeb.NotFoundError, gettext("Delivery not found"), fn -> 18 | get(conn, "/map?token=invalid_token") 19 | end 20 | end 21 | 22 | test "sending and receiving resource update events", %{conn: conn} do 23 | %{id: id, public_auth_key: public_auth_key, from_name: _from_name, to_name: _to_name} = 24 | delivery_resource = 25 | TololoCore.Deliveries.Delivery.empty!(authorize?: false) 26 | |> TololoCore.Deliveries.Delivery.update_state!(:In_Preparation, authorize?: false) 27 | |> TololoCore.Deliveries.Delivery.update_state!(:Ready_To_Pickup, authorize?: false) 28 | |> TololoCore.Deliveries.Delivery.update_state!(:In_Delivery, authorize?: false) 29 | 30 | conn = get(conn, "/map?token=#{public_auth_key}") 31 | {:ok, view, _html} = live(conn) 32 | 33 | {new_lat, new_lng} = {1234.0, 5678.0} 34 | 35 | topic = "delivery:updated:#{id}" 36 | Phoenix.PubSub.subscribe(Tololo.PubSub, topic) 37 | 38 | TololoCore.Deliveries.Delivery.update_location!(delivery_resource, new_lat, new_lng, 39 | authorize?: false 40 | ) 41 | 42 | assert_received(%{topic: ^topic}) 43 | 44 | assert_push_event(view, "phx:resource_update", %{ 45 | resource: %{ 46 | current_pos: [^new_lat, ^new_lng] 47 | } 48 | }) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /vector.yaml: -------------------------------------------------------------------------------- 1 | sources: 2 | in: 3 | type: "stdin" 4 | 5 | sinks: 6 | out: 7 | inputs: 8 | - "in" 9 | type: "console" 10 | encoding: 11 | codec: "text" 12 | loki: 13 | type: loki 14 | encoding: 15 | codec: "text" 16 | inputs: 17 | - "in" 18 | labels: 19 | source: "tololo" 20 | endpoint: http://localhost:3100 21 | --------------------------------------------------------------------------------
8 | v{Application.spec(:tololo, :vsn)} 9 |