Skip to content

Shopifex is a Fireside component that implements core e-commerce functionality for your Elixir app.

License

Notifications You must be signed in to change notification settings

ibarakaiev/shopifex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Shopifex

Shopifex is a Fireside component that implements e-commerce functionality. To install it in your Elixir app, make sure to add :fireside to your list of dependencies and run:

mix fireside.install shopifex@github:ibarakaiev/shopifex

Note

You need to have a supervision tree in your Elixir app for Shopifex to work. If you are creating a new Mix project, make sure to pass the --sup flag.

Admin panel

Shopifex enables ash_admin. If you use Phoenix, follow the AshAdmin tutorial to enable the admin routes in your app.

Usage

Consult the tests to see example usage.

Resources

Products

Products may have multiple Attributes which may have several AttributeOptions. Example: shoes may have two attributes, size and color, which have many attribute options (size options and different colors). Attribute options may also introduce an additional charge.

Additionally, each Product needs at least one ProductVariant which defines a title, description, handle (displayed in the URL), etc. Each ProductVariant in turn needs at least one PriceVariant, which defines a price. A PriceVariant may belong to multiple ProductVariants, but not to multiple products, and a Product may not have multiple identical Prices.

This architecture allows testing different titles and descriptions, as well as different prices (independent of titles and descriptions).

Product has a display_product_variant function, which works as follows:

  • if product_variant_id is passed, that product variant will be loaded.
  • otherwise:
    • if selected_product_variant_id is set, that product variant will be used.
    • otherwise, the oldest existing product variant will be loaded.

ProductVariant has a similar display_price_variant function. Additionally, it has a compare_at_price function, which returns the most expensive price variant. This is useful when a discount is available, and the highest price can be shown as the "original" price.

Additionally, Product has helper methods to retrieve the title, description, and price, which all rely on the display_product_variant (and have the same function signature).

A product can also be :static or :dynamic. If a product is :dynamic, an entire new Ash resource may be used to implement it for complex use cases such as Memory Trivias by Memory+ which use Shopifex.

classDiagram
    class Attribute {
        UUID id
        update(String title, String alias)
        destroy()
        read()
        create(Map[] options, String title, String alias)
        add_options(Map[] options)
        by_id(UUID id)
        by_alias(String alias)
    }
    class AttributeOption {
        UUID id
        String value
        String text
        Money additional_charge
        UUID attribute_id
        Attribute attribute
        update(String value, String text, Money additional_charge, UUID attribute_id)
        create(String value, String text, Money additional_charge, UUID attribute_id)
        destroy()
        read()
    }
    class PriceVariant {
        UUID id
        Money price
        UUID product_id
        Product product
        read()
        create(Money price, UUID product_id)
        by_id(UUID id)
    }
    class Product {
        UUID id
        ProductStatus status
        String handle
        ProductType type
        UUID selected_product_variant_id
        ProductVariant selected_product_variant
        destroy()
        read()
        create(Map[] product_variants, Map[] attributes, String handle, ProductType type)
        add_product_variants(Map[] product_variants)
        add_attributes(Map[] attributes)
        select_display_product_variant(UUID selected_product_variant_id)
        update_status(ProductStatus status)
        by_id(UUID id)
        by_handle(String handle)
    }
    class ProductAttributes {
        update()
        create()
        destroy()
        read()
    }
    class ProductVariant {
        UUID id
        String alias
        String title
        String description
        String[] image_urls
        UUID selected_price_variant_id
        UUID product_id
        Product product
        PriceVariant selected_price_variant
        update(String title, String description, String alias, String[] image_urls)
        destroy()
        read()
        create(Map[] price_variants, String alias, String title, String description, ...)
        add_price_variant(Map[] price_variants)
        select_display_price_variant(UUID selected_price_variant_id)
        by_id(UUID id)
        by_alias_and_product_id(String alias, UUID product_id)
    }
    class ProductVariantPriceVariant {
        update()
        create()
        destroy()
        read()
    }

    Attribute -- AttributeOption
    Attribute -- Product
    Attribute -- ProductAttributes
    PriceVariant -- Product
    PriceVariant -- ProductVariant
    PriceVariant -- ProductVariantPriceVariant
    Product -- ProductAttributes
    Product -- ProductVariant
    ProductVariant -- ProductVariantPriceVariant
Loading

Carts

A Cart contains multiple CartItems which record what ProductVariant and PriceVariant was used (and a dynamic_product_id if the product is dynamic).

Cart items are added with Cart.add_cart_item. If a cart item is already in the cart, its quantity will be increased. An individual cart item's quantity can be changed with CartItem.update_quantity.

CartItem implements several helper methods:

  • subtotal: the price of a cart item (price of a product with all additional charges times the quantity).
  • compare_at_subtotal: what the subtotal would have been if the compare-at price was used
  • display_title: what title to display in the cart. If a cart item is associated with a dynamic product, a dynamic title will be used.
  • display_description: same as above, but for description.

A Cart also has associated CheckoutSessions. It has two functions to aid with creating and maintaining checkout sessions:

  • add_new_checkout_session: adds a new CheckoutSession and invalidates all previous ones.
  • expire_all_checkout_sessions: invalidates all existing associated checkout sessions.

All changes are broadcasted to PubSub so that the cart stays as up-to-date as possible if LiveView is used.

classDiagram
    class Cart {
        UUID id
        Atom state
        update(Atom state)
        create(Atom state)
        destroy()
        read()
        add_to_cart(Map cart_item)
        add_new_checkout_session()
        expire_all_checkout_sessions()
        complete_checkout()
        by_id(UUID id)
    }
    class CartItem {
        UUID id
        Integer quantity
        ProductType product_type
        UUID dynamic_product_id
        UUID cart_id
        UUID product_variant_id
        UUID price_variant_id
        Cart cart
        ProductVariant product_variant
        PriceVariant price_variant
        update()
        read()
        create_or_increment_quantity(Map product_variant, Map price_variant, ProductType product_type, UUID dynamic_product_id)
        update_quantity(Integer quantity)
        by_id(UUID id)
        destroy()
    }

    Cart -- CartItem
    Cart -- CheckoutSession
    CartItem -- PriceVariant
    CartItem -- ProductVariant
Loading

Checkout

Shopifex doesn't implement checkout session functionality, but exposes a CheckoutSession resource to make it easier to track active checkout sessions and invalidate inactive ones if the cart's contents change. It is up to the end user to implement checkout functionality, either via Stripe Checkout Sessions or something else. For example, you may add a StripeCheckoutSession (in a new Stripe Ash domain) that belongs_to a CheckoutSession.

classDiagram
    class CheckoutSession {
        UUID id
        UUID cart_id
        Atom state
        Cart cart
        create(UUID cart_id)
        read()
        by_id(UUID id)
        complete_checkout()
        expire()
    }

    Cart -- CheckoutSession
Loading

Orders

Shopifex doesn't make assumptions about how orders are placed, tracked, and fulfilled. It is recommended to create a new Orders domain which will contain orders created from successful checkout sessions.

About

Shopifex is a Fireside component that implements core e-commerce functionality for your Elixir app.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages