Development – Kinane https://djang0.dev Sat, 13 Dec 2025 11:02:22 +0000 en-US hourly 1 https://wordpress.org/?v=6.3.2 https://djang0.dev/wp-content/uploads/2021/10/code-solid.svg Development – Kinane https://djang0.dev 32 32 Implementing an effective billing system using Finite State Machines https://djang0.dev/development/implementing-a-billing-system-using-finite-state-machines/ Fri, 08 Nov 2024 22:13:06 +0000 https://djang0.dev/?p=1118 Reading Time: 5 minutes Implementing a whole billing system for Shopify, backend and frontend using XState and the actor model]]> Reading Time: 5 minutes
  • This post is intended for implementing a recurring billing system for Shopify; if you're looking for an OOB alternative, check Mantle (it's dope). I did this code before Mantle was as good as it is right now, but it's still generically useful (if there's no mantle in your niche)
  • It's better if you have already implemented a billing system and, more importantly, if you understand XState and the Actor Model (I want to have the same PTSD as my readers πŸ‘‰πŸ‘ˆ)
  • Check this article on a large screen because you're going to eat a lot of stately chart.s

Understanding the problematic

Implementing a billing system sucks; you've to manage a lot of edge cases like this smart guy says:

What happens if you want to handle:

image 1
(it's not; it's delicious pls trust me)
  • Discount?
  • Upgrade/Downgrade?
  • What happens during app uninstall?
  • Jobs, can you check if the subscription is still active?
  • Remember how it works when you return to this random codebase 1 month later?
  • Making things evolve easily
  • Centralizing your logic
  • Removing all boring side effects?
  • Sort of composition?
  • Using the same mental model front/back?
  • Even managing the Front? (bonus πŸ₯ΈπŸ₯ΈπŸ₯Έ)

Answer: Finite State Machines + Actor Model

Definitions

Entities

Here are the entities we'll use for managing our billing system

Recurring Application Charge (RAC)

It represents a recurring charge (gg wp), which is linked to an appPlan and maybe an appDiscount

It could be active or not; only one RAC can be active, BUT a shop can have many RACs (pending, canceled etc.)

TypeScript
export const RecurringApplicationCharge = z
  .object({
    id: z.string(),
    name: z.string(), // refer to AppPlan.name
    price: z.number().int().min(0),
    appDiscountId: z.string().uuid().nullable(),
    createdReason: CreateDeleteRacReasonEnum,
    billingOn: zDate.nullable(),
    status: RacStatusEnum,
    activatedOn: zDate.nullable(),
    returnUrl: z.string(),
    test: z.boolean(),
    cancelledOn: zDate.nullable(),
    cancelledReason: CreateDeleteRacReasonEnum.nullable(),
    isCancellationProrated: z.boolean().nullable(),
    trialDays: z.number().int().min(0),
    trialEndsOn: zDate.nullable(),
    shopifyShopId: z.string(),
    apiClientId: z.string(),
    currency: z.enum(["USD", "AUD", "EUR", "INR", "SGD", "GBP", "CAD"]),
  })
  .merge(BaseShopifyEntitiesKeys)

appPlan

Representing the plans with intervals available in the app

TypeScript
export const AppPlan = z.object({
  id: z.string().uuid(),
  name: z.string(),
  slug: z.string(),
  trialDays: z.number().int(),
  interval: z.enum(["EVERY_30_DAYS", "ANNUAL", "LIFETIME"]),
  price: z.number().int(),
  i18nFeatures: z.record(z.string(), z.array(z.string())),
  description: z.record(z.string(), z.string()),
  currencyCode: z.enum(["USD", "AUD", "EUR", "INR", "SGD", "GBP", "CAD"]),
  updatedAt: zDate,
  createdAt: zDate,
  activeSizeChartsLimit: z.number().int().min(-1),
  includedShopifyPlans: z.array(FullPlanTypes),
  testToProdGracePeriodDays: z.number().int().min(0),
  features: z.record(
    z.string(),
    z.object({
      enabled: z.boolean(),
    }),
  ),
  type: z.enum(["common", "fallback", "custom"]),
  watermarkStatus: WatermarkStatus,
})

appDiscount

Represent a discount, can be allowed to N plans

TypeScript
export const AppDiscount = z.object({
  id: z.string(),
  allowedPlanSlugs: z.array(z.string()),
  description: z.string(),
  discountCode: z.string(),
  discountUsageLimit: z.number().min(-1),
  discountUsed: z.number().min(0),
  discountValue: z.number().min(0),
  discountType: z.enum(["percentage", "fixed_amount"]),
  durationLimitInterval: z.number().min(-1),
  createdAt: zDate,
  updatedAt: zDate,
})

Relations

image
Implementing an effective billing system using Finite State Machines 5

Processing the Subscription

Initializing the subscription creation

The idea is to reach the shopify paywall before approving the billing in a safe condition:

We can first simplify the machine:

You can click on simulate and on the current state to test the workflow. Also, check the state's description because it can explain what it does.

So we have, again you can check the description on stately :

  1. Validating, which checks if the shop can initialize the charge
  2. creatingSubscription which does a call to Shopify to initialize the payment
  3. If it's done => success; otherwise error

It's pseudo-machine that describes the key steps of initializing the subscription, with the key steps written down.

Let's add a bit more complexity here and create the initCreateSubscriptionMachine (take some time to check it; there's a description for each state)

I'm not commenting since it should be descriptive (I mean, that's the goal πŸ₯Έ)

Of course, it takes time to get used to Stately‘s representation, there's arrows everywhere + it looks bloated, but even non-techies can UNDERSTAND this BEAUTY with time.

So far, so good. We have the first step, and we generated this appSubscription and we get the callbackUrl to validate the payment

So now the user is redirected here. We have the RAC persisted etc. etc cool, nice THANKS MOM

CleanShot 2024 11 08 at 20.38.01@2x
Implementing an effective billing system using Finite State Machines 6

Let's go for the second part when the user clicks on approve (remember this for later)

Managing the billing confirmation

We get back the webhook app_subscriptions/update who'll trigger the confirmSubscriptionMutation:

It's pretty sequential, yes. We retrieve the RAC from Shopify, persist, and sync. There's just one thing here, confirmSubscriptionMutation invoke a syncShopAppPlanMachine in syncShopAppPlanToRemote state that does a synchronization to Shopify (updating a billing metafield to remove the watermark)

🚨 So it's a machine invoking a machine πŸ‘‰ an actor invoking an actor AND that's the power of XState

In this case, It induces a sort of composability because we only invoke it (it's like fire/forget), but it's more than that

Updating back to Shopify

We retrieve data and update a shopify metafield accordingly, using a machine again syncShopAppPlanMachine

Liquid
{% liquid
    assign billingConfigRaw = app.metafields.rsc-app.billing_config.value
    assign billingConfig = billingConfigRaw | split: ","
    assign appPlanSlug = billingConfig | where: "appPlanSlug" | first | split: ":" | last
    assign watermarkStatus = billingConfig | where: "watermarkStatus" | first | split: ":" | last
    assign activeSizeChartsLimit = billingConfig | where: "activeSizeChartsLimit" | first | split: ":" | last | plus: 0
%}
image 2
By updating this metafield based on the plan, we remove the watermark on the liquid part

Combining the full workflow

Now that we have divided each step, we can merge back into a single machine based on the previous machines to create kind of a stateful workflow.

  • Waiting wait until an event is fired
  • on event billing.create.init we save the subscription on the context and transit to InitAppSubscription
  • InitAppSubscription fire the first machine we talked about initCreateSubscriptionMachine
  • on event subscription.create.confirm (which is when a user approves) we trigger confirmSubscriptionMutationMachine

And so, finally, we have the full workflow 😎:

By splitting key parts into machines (init, confirm, pushing back to Shopify), we created a whole composable, upgradeable, and more importantly, READABLE (yes, I swear you're going to be used to this Stately madness) set of machines ready to use for different intents.

Bonus: Managing the app Frontend with an FSM using the inspector

Let's continue with one other bloated schema, which is managing the front plan page with FSM; thanks to Stately Inspector, you can see the Sequence Diagram of each event triggered

Related machine:

OF COURSE, this machine can be tested with Model Based Testing based on the definition itself! 😎

Conclusion

We created a declarative and centralized workflow to manage a simple billing system with XState and composability in mind.

Again, getting used to XState Stately takes time, but it's worth the price, especially if you work as a team.

There are advantages in clarity and minimizing your mental load using XState.

There are also cons, like sometimes coding itself, the shape of the machine can be overwhelming (for instance, you should use a guard while a pattern matcher could've been more straightforward, or just if/else). That's the price of being able to represent a workflow; you should follow common rules.

]]>
Initialize the subscription of the billing system nonadult
Model-Based Testing for a Secure Shopify Theme with XState https://djang0.dev/development/model-based-testing-to-shopify-theme/ https://djang0.dev/development/model-based-testing-to-shopify-theme/#respond Tue, 17 Oct 2023 12:10:33 +0000 https://djang0.dev/?p=937 Reading Time: 7 minutes Discussing testing methods with Model-Based Testing and trying to simulate a customer workflow based on key steps in his journey using Playwright and FSM.]]> Reading Time: 7 minutes

The repository is accessibleΒ and has been updated with the latest major XState version.

Model-based testing (MBT) involves creating a model that describes how a system should work and then using this model to generate tests automatically. The model is often a state machine, representing the stages or states the system can reach and the transitions between them. Ok this a bit boring, A concrete example would be to represent the different possible paths of a user on an e-commerce site (a bit like when you spy on Hotjar and see the navigation steps of a customer).

Basic Example

Simple representation of a basic machine which goes to home page and then shop all page

Those charts are representations from Stately; it's generated based on my machine's code, there's also a vscode plugin to represent your machine

From this model, we can derive the states and transitions:

States

  1. onHomePage: User will be redirected to the Home Page
  2. inShopAllPage: The user is on the “Shop All” page.

Events

  1. HEADER_MENU_GO_TO_HOME_PAGE: Transition from any page back to the home page via the header menu.
  2. HERO_GO_TO_SHOP_ALL: Transition from the home page to the “Shop All” page via the hero button or section.

With these events and states, we can impute the playwrights' related actions.

{
  states: {
    onHomePage: async () => {
      await page.goto(`${baseShopUrl}/`);
    },
    inShopAllPage: async () => {
      await page.waitForURL(`${baseShopUrl}/collections/all`);
    },
  },
  events: {
    HERO_GO_TO_SHOP_ALL: async () => {
      const heroButton = await page.$(".banner a");
      if (!heroButton) throw new Error("Hero button not found");
      await heroButton.click();
    },
    HEADER_MENU_GO_TO_HOME_PAGE: async () => {
      const homeButton = await page.$("#HeaderMenu-home");
      if (!homeButton) throw new Error("Home button not found");
      await homeButton.click();
    },
  },
}

How many paths (and thus tests) do you think this will generate? 1? 2? even more?

Answer: Only one πŸ˜…

In MBT, each state is unique, so the return path is not set up as a standard

To do so, we need to create a “unique” state:

CleanShot 2023 10 18 at 11.40.25@2x
Model-Based Testing for a Secure Shopify Theme with XState 11

Here we go, using @xstate/test and playwright

import { test } from "@playwright/test"
import { createTestMachine, createTestModel } from "@xstate/test"
import { e2eConfig } from "./config"

const machine = createTestMachine({
  id: "basicHomePage",
  initial: "onHomePage",
  states: {
    onHomePage: {
      on: {
        HERO_GO_TO_SHOP_ALL: {
          target: "#basicHomePage.inShopAllPage",
          reenter: false,
        },
      },
    },
    inShopAllPage: {
      on: {
        HEADER_MENU_GO_TO_HOME_PAGE: {
          target: "#basicHomePage.inHomePageDestination",
          reenter: false,
        },
      },
    },
    inHomePageDestination: {
      type: "final",
    },
  },
})

createTestModel(machine)
  .getSimplePaths()
  .forEach((path) => {
    test(path.description, async ({ page }) => {
      await path.test({
        states: {
          onHomePage: async () => {
            await page.goto(`${e2eConfig.localBaseShopUrl}/`)
          },
          inHomePageDestination: async () => {
            await page.waitForURL(`${e2eConfig.localBaseShopUrl}/`)
          },
          inShopAllPage: async () => {
            await page.waitForURL(`${e2eConfig.localBaseShopUrl}/collections/all`)
          },
          inShopAllPageDestination: async () => {
            await page.waitForURL(`${e2eConfig.localBaseShopUrl}/`)
          },
        },
        events: {
          HERO_GO_TO_SHOP_ALL: async () => {
            const heroButton = await page.$(".banner a")
            if (!heroButton) throw new Error("Hero button not found")
            await heroButton.click()
          },
          HEADER_MENU_GO_TO_HOME_PAGE: async () => {
            const homeButton = await page.$("#HeaderMenu-home")
            if (!homeButton) throw new Error("Home button not found")
            await homeButton.click()
          },
        },
      })
    })
  })

In this way, We go back to the first theoretical state onHomePage:

  1. The user starts on the home page (onHomePage).
  2. The user clicks on the hero button or section (HERO_GO_TO_SHOP_ALL).
  3. The user is navigated to the “Shop All” page (inShopAllPage).
  4. The user then clicks on the header menu to return to the home page (HEADER_MENU_GO_TO_HOME_PAGE).
  5. The user ends back on the home page (inHomePageDestination).

onHomePage -> HERO_GO_TO_SHOP_ALL -> inShopAllPage -> HEADER_MENU_GO_TO_HOME_PAGE -> inHomePageDestination

Round-trip result 🏎️

Declarative > Imperative

By specifying the various states and events, we give control over the generation of test steps to the model, rather than making it explicit ourselves.

Advanced Structure using Model-Based Testing on a Cart Drawer

We can now generate the user workflow that wants, for instance, to buy a product using different paths

Model-Based testing stately representation on a shopify Cart Drawer
Model-Based Testing for a Secure Shopify Theme with XState 12
import { expect, test } from "@playwright/test"
import { createTestModel } from "@xstate/test"
import { setup } from "xstate"
import { e2eConfig } from "./config"
import { genPwMetas, generateXstateStateAndEventsFromPw } from "./utils"

const singleProductPageBaseUrl = `${e2eConfig.localBaseShopUrl}/products/the-3p-fulfilled-snowboard`

const advancedAddToCartMachine = setup({
  types: {
    events: {} as
      | {
          type: "ADD_TO_CART"
        }
      | {
          type: "BUY_NOW"
        }
      | {
          type: "SHOW_CART"
        }
      | {
          type: "CLICK_ON_VIEW_CART_PAGE_BUTTON"
        }
      | {
          type: "CLICK_ON_HIDE_CART"
        }
      | {
          type: "CLICK_ON_CART_ICON"
        }
      | {
          type: "CHECK_PRODUCT_ADDED_TO_CART"
        }
      | {
          type: "GO_TO_CHECKOUT"
        },
  },
}).createMachine({
  initial: "onProductPage",
  id: "atcMachine",
  context: {},
  states: {
    // This act as our entrypoint
    onProductPage: {
      on: {
        ADD_TO_CART: "cartDrawer.visible",
        BUY_NOW: "inCheckout",
      },
    },
    cartDrawer: {
      initial: "hidden",
      states: {
        hidden: {
          on: {
            CLICK_ON_CART_ICON: "#atcMachine.inCartPage",
          },
        },
        visible: {
          on: {
            CLICK_ON_HIDE_CART: "hidden",
            CLICK_ON_CART_ICON: "#atcMachine.inCartPage",
            CLICK_ON_VIEW_CART_PAGE_BUTTON: "#atcMachine.inCartPage",
          },
        },
      },
    },
    productAddedToCart: {
      on: {
        GO_TO_CHECKOUT: "inCheckout",
      },
    },
    inCartPage: {
      on: {
        GO_TO_CHECKOUT: "inCheckout",
      },
    },
    inCheckout: {
      type: "final",
    },
  },
})

// Not quite sure of this pattern compared to simply use `meta`
const gen = genPwMetas<typeof advancedAddToCartMachine>((page) => {
  return {
    states: {
      inCartPage: {
        meta: {
          urlRegex: new RegExp(
            `^${e2eConfig.localBaseShopUrl.replace(/[-\/\^$*+?.()|[\]{}]/g, "\\$&")}/cart`,
            "i",
          ),
          waitFor: true,
          expectUrl: true,
        },
      },
      onProductPage: {
        decoratedHandler: async () => {
          await page.goto(singleProductPageBaseUrl)
          await page.waitForURL(singleProductPageBaseUrl)
        },
      },
      "cartDrawer.visible": async () => {
        await page.waitForSelector("#cart-notification")
        const cartDrawer = await page.$("#cart-notification")
        if (!cartDrawer) throw new Error("Cart drawer not found")
        const isActive = cartDrawer.evaluate((node) => node.classList.contains("active"))
        expect(isActive).toBeTruthy()
      },
      addedToCart: async () => {
        const cartItem = await page.$("#cart-notification-product")
        if (!cartItem) throw new Error("Cart item not found")
        const titleText = await cartItem.evaluate(async (node) => {
          const title = node.querySelector("h3")
          if (!title) throw new Error("Cart item title not found")
          return title.textContent
        })
        expect(titleText).toBe("The 3P Fulfilled Snowboard")
      },
      inCheckout: {
        meta: {
          urlRegex: new RegExp(
            `^(?:${e2eConfig.localBaseShopUrl.replace(
              /[-\/\^$*+?.()|[\]{}]/g,
              "\\$&",
            )}|${e2eConfig.webBaseShopUrl.replace(
              /[-\/\^$*+?.()|[\]{}]/g,
              "\\$&",
            )})/checkouts/.*$`,
            "i",
          ),
          waitFor: true,
          expectUrl: true,
        },
      },
      hidden: {},
    },
    events: {
      ADD_TO_CART: async () => {
        const addToCartButton = await page.$('button[name="add"]')
        if (!addToCartButton) throw new Error("Add to cart button not found")
        await addToCartButton.click()
      },
      GO_TO_CHECKOUT: async () => {
        const goToCheckoutButton = await page.$("#checkout")
        if (!goToCheckoutButton) throw new Error("Go to checkout button not found")
        await goToCheckoutButton.click()
      },
      BUY_NOW: async () => {
        const buyNowButton = await page.$(
          ".shopify-payment-button__button.shopify-payment-button__button--unbranded",
        )
        if (!buyNowButton) throw new Error("Buy now button not found")
        await buyNowButton.click()
      },
      SHOW_CART: async () => {
        const cartButton = await page.$("#cart-notification-button")
        if (!cartButton) throw new Error("Cart button not found")
        await cartButton.click()
      },
      CLICK_ON_VIEW_CART_PAGE_BUTTON: async () => {
        const viewCartPageButton = await page.$("#cart-notification-button")
        if (!viewCartPageButton) throw new Error("View cart page button not found")
        await viewCartPageButton.click()
      },
      CLICK_ON_HIDE_CART: async () => {
        const hideCartButton = await page.$(".cart-notification__close")
        if (!hideCartButton) throw new Error("Hide cart button not found")
        await hideCartButton.click()
      },
      CLICK_ON_CART_ICON: async () => {
        const cartIcon = await page.$("#cart-icon-bubble")
        if (!cartIcon) throw new Error("Cart icon not found")
        await cartIcon.click()
      },
    },
  }
})

createTestModel(advancedAddToCartMachine)
  .getSimplePaths()
  .forEach((path) => {
    test(path.description, async ({ page }) => {
      await path.test(generateXstateStateAndEventsFromPw(gen(page), page))
    })
  })

And generate a tiny factory for the related events and states to bind the playwright's logic:

const advancedAddToCartMachinePlaywrightStatesAndEventsFactory = (
  page: Page
): StateAndEvents => {
  return {
    states: {
      inCartPage: {
        decoratedHandler: async () => {},
        meta: {
          urlRegex: new RegExp(
            `^${baseShopUrl.replace(/[\/\^$*+?.()|[\]{}-]/g, "\\$&")}/cart`,
            "i"
          ),
          waitFor: true,
          expectUrl: true,
        },
      },
      onProductPage: {
        decoratedHandler: async () => {
          await page.goto(singleProductPageBaseUrl);
          await page.waitForURL(singleProductPageBaseUrl);
        },
      },
      "cartDrawer.visible": async () => {
        await page.waitForSelector("#cart-notification");
        const cartDrawer = await page.$("#cart-notification");
        if (!cartDrawer) throw new Error("Cart drawer not found");
        const isActive = cartDrawer.evaluate((node) =>
          node.classList.contains("active")
        );
        expect(isActive).toBeTruthy();
      },
      addedToCart: async () => {
        const cartItem = await page.$("#cart-notification-product");
        if (!cartItem) throw new Error("Cart item not found");
        const titleText = await cartItem.evaluate(async (node) => {
          const title = await node.querySelector("h3");
          if (!title) throw new Error("Cart item title not found");
          const titleText = title.textContent;
          return titleText;
        });
        expect(titleText).toBe("The 3P Fulfilled Snowboard");
      },
      inCheckout: {
        meta: {
          urlRegex: new RegExp(
            `^${baseShopUrl.replace(
              /[-\/\^$*+?.()|[\]{}]/g,
              "\\$&"
            )}/checkouts/.*$`,
            "i"
          ),
          waitFor: true,
          expectUrl: true,
        },
      },
      hidden: {},
    },
    events: {
      ADD_TO_CART: async () => {
        const addToCartButton = await page.$('button[name="add"]');
        if (!addToCartButton) throw new Error("Add to cart button not found");
        await addToCartButton.click();
      },
      GO_TO_CHECKOUT: async () => {
        const goToCheckoutButton = await page.$('button[name="checkout"]');
        if (!goToCheckoutButton)
          throw new Error("Go to checkout button not found");
        await goToCheckoutButton.click();
      },
      BUY_NOW: async () => {
        const buyNowButton = await page.$(
          ".shopify-payment-button__button.shopify-payment-button__button--unbranded"
        );
        if (!buyNowButton) throw new Error("Buy now button not found");
        await buyNowButton.click();
      },
      SHOW_CART: async () => {
        const cartButton = await page.$("#cart-notification-button");
        if (!cartButton) throw new Error("Cart button not found");
        await cartButton.click();
      },
      CLICK_ON_VIEW_CART_PAGE_BUTTON: async () => {
        const viewCartPageButton = await page.$("#cart-notification-button");
        if (!viewCartPageButton)
          throw new Error("View cart page button not found");
        await viewCartPageButton.click();
      },
      CLICK_ON_HIDE_CART: async () => {
        const hideCartButton = await page.$(".cart-notification__close");
        if (!hideCartButton) throw new Error("Hide cart button not found");
        await hideCartButton.click();
      },
      CLICK_ON_CART_ICON: async () => {
        const cartIcon = await page.$("#cart-icon-bubble");
        if (!cartIcon) throw new Error("Cart icon not found");
        await cartIcon.click();
      },
    },
  };
}

With some tiny tricks, we can be in pure declarative, trying to further abstract the playwright's logic and start some tests (this is a bit boring and not essential), by using some metadata.

import { test } from "@playwright/test";
import { createTestMachine, createTestModel } from "@xstate/test";

const isAPlaywrightStateFunction = (
  state: PlaywrightState
): state is PlaywrightFunctionState => {
  return isFunction(state);
};
const generateXstateTestStateFromPlaywrightState = (
  playwrightState: PlaywrightState,
  page: Page
): any => {
  if (isAPlaywrightStateFunction(playwrightState)) {
    return async () => {
      await playwrightState();
    };
  }
  const meta = playwrightState.meta;
  return async () => {
    if (playwrightState?.decoratedHandler) {
      await playwrightState.decoratedHandler();
    }
    if (meta?.waitFor && meta.urlRegex) {
      await page.waitForURL(meta.urlRegex);
    }
    if (meta?.expectUrl) {
      expect(page.url()).toMatch(meta.urlRegex);
    }
  };
};
const generateXstateStateAndEventsFromPlaywrightStatesAndEvents = (
  playwrightStatesAndEvents: StateAndEvents,
  page: Page
) => {
  const states = {};
  const events = {};
  Object.keys(playwrightStatesAndEvents.states).forEach((key) => {
    const playwrightState = Reflect.get(playwrightStatesAndEvents.states, key);
    Reflect.set(
      states,
      key,
      generateXstateTestStateFromPlaywrightState(playwrightState, page)
    );
  });
  Object.keys(playwrightStatesAndEvents.events).forEach((key) => {
    const playwrightEvent = Reflect.get(playwrightStatesAndEvents.events, key);
    Reflect.set(events, key, playwrightEvent);
  });
  return {
    states,
    events,
  };
};

createTestModel(advancedAddToCartMachine)
  .getSimplePaths()
  .forEach((path) => {
    test(path.description, async ({ page }) => {
      await path.test(
        generateXstateStateAndEventsFromPlaywrightStatesAndEvents(
          advancedAddToCartMachinePlaywrightStatesAndEventsFactory(page),
          page
        )
      );
    });
  });

This gives us five paths to be tested:

CleanShot 2023 10 18 at 13.40.39@2x
Model-Based Testing for a Secure Shopify Theme with XState 13

Shortest Paths vs Simplest Paths?

Shortest Paths

  • The shortest path between two points (or states) without passing through the same point twice.
  • Often used to identify the fastest or most efficient path.
  • Does not necessarily cover all test cases or features.

Simplest Paths

  • Any path that passes from a starting point to an end point without revisiting any intermediate point.
  • Unlike “shortest path”, it may not be the shortest path, but it ensures that no node is visited more than once.
  • Can cover more features or test cases than the “shortest path”, as it can take longer or different routes.

Translated with www.DeepL.com/Translator (free version)

Results

The model automatically generated five tests for us with a consistent workflow 🀯

Conclusion

Using a finite state machine and playwright, we generated e2e tests by targeting the key steps of a user journey and creating the various associated paths.

Benefits:

  • Self-generated test (even if GitHub copilot works fine as usual)
  • Ultra-fast maintenance: if you want to add a step along the way, the machine can be changed very quickly.
  • High Test Coverage
  • Easy to automate
  • More reliable tests
  • Explicit docs
  • Helps you to shine in society 🀠

Cons:

  • It hurts to learn
]]>
https://djang0.dev/development/model-based-testing-to-shopify-theme/feed/ 0 Development - Kinane nonadult
Finite State Machines in E-commerce by Example https://djang0.dev/development/finite-state-machines-in-e-commerce-by-example/ https://djang0.dev/development/finite-state-machines-in-e-commerce-by-example/#respond Mon, 11 Sep 2023 04:32:55 +0000 https://djang0.dev/?p=906 Reading Time: 2 minutes How do you organize your business logic with finite-state machines? Here is a brief introduction to the concept applied to e-commerce]]> Reading Time: 2 minutes

How do you organize your business logic with finite-state machines? Here is a brief introduction to the concept applied to e-commerce:

🚦 What is a State Machine?

Imagine an intersection with traffic lights to regulate the passage of cars and pedestrians. The light can be:

– Red πŸ”΄ β‡’ In this state, cars must stop, and pedestrians can cross.

– Yellow 🟑 β‡’ In this state, cars should prepare to stop, and pedestrians should not start crossing.

– Green 🟒 β‡’ In this state, cars can proceed, and pedestrians can't cross.

In a state machine, finite states represent specific conditions and transitions between them must follow a logical sequence. You can't jump directly from πŸ”΄ (stop) to 🟒 (go), or from 🟒 to 🟑 (prepare to stop).

πŸ’‘ Why Using State Machines?

– Clarity: State machines break down your application into manageable states, making the logic easier to read and understand.

– Reliability: Transitions between states are explicit, which reduces the likelihood of bugs.

– Scalability: Introducing a new feature or state becomes simpler, making your codebase more maintainable.

Actually, finite-state machines are not just for software engineers but a useful framework for thinking about processes and systems.

πŸ›’ A Detailed Example with an E-Commerce Cart

Loading State: When the cart is accessed, it's in a ‘loading' state. Here, an ‘invoke' fetches items πŸ“¦ from a service. Upon success, the cart transitions to the ‘idle' state and updates its context (think of it as a mini-store) with the fetched items.

Idle State: Think of ‘idle' as a state of readiness. The cart has finished loading, has its data, and is waiting for the user to initiate events.

 – Actions: When an item is added, removed, or its quantity updated, actions are executed to mutate the context πŸ”„, thanks to events that trigger these actions. (The event ‘REMOVE_ITEM' triggers the action ‘removeItem' that mutates the context.)

 – Context: The context stores the cart items and a flag to indicate whether the terms and conditions are accepted βœ….

goToCheckout State: To transition to ‘goToCheckout,' a guard condition checks if the terms are accepted 🚫. If yes, the cart moves to this state to complete the purchase πŸ›οΈ.

In the end, we can represent all the logic of our cart within a machine in a single place with a formalized and highly deterministic approach. This minimizes side effects. Also, this is just a glimpse of what an FSM can do, followed by parallel state logic, actor model, parent-to-child communication, and so on.

A detailed example of a FSM with an e-commerce cart

Docs

]]>
https://djang0.dev/development/finite-state-machines-in-e-commerce-by-example/feed/ 0 Development - Kinane nonadult
Why do Shopify Functions provide an unfair advantage to create exceptional stores? https://djang0.dev/development/why-do-shopify-functions-provide-an-unfair-advantage-to-create-exceptional-stores/ https://djang0.dev/development/why-do-shopify-functions-provide-an-unfair-advantage-to-create-exceptional-stores/#respond Fri, 10 Mar 2023 17:21:53 +0000 https://djang0.dev/?p=842 Reading Time: 2 minutes This article will try to summarize my experience with Shopify Functions; here we go and why they provide an unfair advantage to create exceptional stores. ]]> Reading Time: 2 minutes

1 – What is a Shopify Function?

Shopify Function allows Shopify app developers to write custom serverless functions that can be executed in response to certain events or triggers and alter the backend logic of Shopify, opening unlimited possibilities for customizations.

image 4
Why do Shopify Functions provide an unfair advantage to create exceptional stores? 18

e.g., You have an input (for example, a cart), and you return an output (a discount) you want, according to what you have coded in your function depending on the Function API your target. It's like a module in

@make_hq (or your old math lessons): So pretty easy to develop with

image 5
Why do Shopify Functions provide an unfair advantage to create exceptional stores? 19

2 – What are the advantages of Shopify Functions?

  • You are closer to the business logic of Shopify, so you don't need a workaround that will take you a lot of code time to achieve your goals. I made a bundled app based on coupons; now, some logic can be abstracted with Functions.
  • The functions are serverless so Shopify handles all the server load; if you have merchants with huge traffics, the AWS bill will sting less.
  • You can iterate easier. Each function is a brick of your application, more easily maintainable.
  • You can directly use meta fields to store data instead of using your database and storing dynamic values while providing a GUI for merchants
image 6
Why do Shopify Functions provide an unfair advantage to create exceptional stores? 20

3 – How to create Shopify Functions

You must choose a language that compiles in WASM. I came from Typescript, So I chose

@AssemblyScript; you can also use Rust or Golang. It will soon be possible to use Typescript with the Shopify compiler (javy) in production.

image 7
Why do Shopify Functions provide an unfair advantage to create exceptional stores? 21

“Something that cannot be done with Javascript will eventually be done with Javascript.”

Sun Tzu

So if you are patient, wait for JS, even if I think it's better to dig into the documentation and try to code quickly

PS: check @grain_lang actively

Resources

Polyglot Functions based on Discount Tutorial: https://github.com/nickwesselman/polyglot-functions/tree/main/extensions…

The tutorial to understand everything: https://shopify.dev/docs/apps/discounts/experience…

@gadget_dev articles https://gadget.dev/blog/understanding-shopify-functions-part-1…

JS Workshop:

https://workshops.shopify.dev/workshops/javascript-functions-preview#0

Conclusion

Thanks for reading, and I hope you found this article useful! Considering the density of things to say, I'll make several articles on the Shopify Functions.

]]>
https://djang0.dev/development/why-do-shopify-functions-provide-an-unfair-advantage-to-create-exceptional-stores/feed/ 0
Why Shopify is more than ever killing the game VS WooCommerce and other Ecommerce platforms? https://djang0.dev/development/why-shopify-is-more-than-ever-killing-the-game-vs-woocommerce-and-other-ecommerce-platforms/ https://djang0.dev/development/why-shopify-is-more-than-ever-killing-the-game-vs-woocommerce-and-other-ecommerce-platforms/#respond Sat, 04 Mar 2023 09:34:21 +0000 https://djang0.dev/?p=784 Reading Time: 3 minutes Discover why I switched to Shopify for my e-commerce business. From better security to easier scaling and promising features, get valuable insights here.]]> Reading Time: 3 minutes

I will summarize my experiences as an app creator and freelance dev in platforms like WooCommerce, Magento, Prestashop, and saleor/medusa. I have reached a tipping point where I no longer prefer to use WooCommerce or similar platforms for the majority of shop projects.

1 – πŸ”’Security: Shopify 🟒

In Ecom, there are big digits that pass through. You clearly have to be paranoid about security. On Shopify, everything is managed so you can work on what really matters (features/management), and so can your client.

One downside of WooCommerce and WordPress is that they are open-source platforms, and the quality of their plugins and technology is subject to vulnerabilities. If you use it for eCommerce and generate revenue, it's crucial to choose managed servers that prioritize security.

image
Why Shopify is more than ever killing the game VS WooCommerce and other Ecommerce platforms? 26

2 – πŸš€ Scaling: Shopify 🟒

Scaling WooCommerce during high-traffic periods means managing server autoscaling, which can be mentally taxing. With a large amount of money potentially coming in during short periods, there is no room for error.

Now imagine running a high-traffic promotion with Kylie, where Shopify's managed platform handles the server management stress, allowing you to focus on sales and watch the blue stars light up on the map with peace of mind.

image 1
Why Shopify is more than ever killing the game VS WooCommerce and other Ecommerce platforms? 27

3 – βš™οΈ Modularity: WooCommerce 🟣 Then but now Shopify 🟒

WooCommerce used to have an advantage over Shopify with its flexibility and customizability through plugins and PHP code.

However, Shopify's app store now offers similar options for gamification, affiliation, and CRO optimization, making it possible to manage most store use cases without the need for customization.

But also native features of Shopify: MetaObjects that replace tools like ACF or Metabox and allow to have a real CMS in Shopify

– BUT OVERALL, the openness of the Shopify Functions, which allows for managing the most complex cases, and the opening of their Backend convinced me as a dev. No more brakes to creating advanced logic. game changer

image 2
Create any type of discount you want with Discount API for Shopify Functions

4 – 🏎️ Fast Iterations: Shopify 🟒

Exit PHP and the disgusting code of some WooCommerce plugins; with Shopify, you can iterate quickly by having maintainable techno and it is not a HELL to evolve. DX is the key.

image 3
One of the best pieces of advice


5 – πŸ“Š Easy management: Shopify 🟒

WooCommerce is a Rube Goldberg machine; you have to understand the back office with no real added value for the merchant; the only advantage you have with WP is the SEO management (And that's where you can wear the hat you want πŸ•΅πŸ»)

6 – β˜„οΈ Futur: Shopify 🟒

For the past few months, Shopify has been announcing a lot of new features, among which: – Headless E-commerce, which is the logical next step for Omnichannel, a more advanced and easily maintainable UX/CRO

– Functions that open the Shopify backend and allow any logic
– Shop.app that puts the customer at the heart of the brand

Conclusion

I switched to Shopify and never looked back. except for Medusa which seems really promising

]]>
https://djang0.dev/development/why-shopify-is-more-than-ever-killing-the-game-vs-woocommerce-and-other-ecommerce-platforms/feed/ 0