diff --git a/.changeset/clean-heads-hear.md b/.changeset/clean-heads-hear.md new file mode 100644 index 0000000..6cb7eeb --- /dev/null +++ b/.changeset/clean-heads-hear.md @@ -0,0 +1,5 @@ +--- +"@codee-sh/medusa-plugin-automations": patch +--- + +update ORDER_ATTRIBUTES to ORDER_QUERY_FIELDS for improved totals calculation and UI consistency diff --git a/.changeset/fresh-lamps-stand.md b/.changeset/fresh-lamps-stand.md new file mode 100644 index 0000000..18e9d62 --- /dev/null +++ b/.changeset/fresh-lamps-stand.md @@ -0,0 +1,5 @@ +--- +"@codee-sh/medusa-plugin-automations": patch +--- + +Update contributing diff --git a/.changeset/fuzzy-coins-invent.md b/.changeset/fuzzy-coins-invent.md new file mode 100644 index 0000000..a30020a --- /dev/null +++ b/.changeset/fuzzy-coins-invent.md @@ -0,0 +1,5 @@ +--- +"@codee-sh/medusa-plugin-automations": patch +--- + +Update name in the column name diff --git a/.changeset/nine-monkeys-serve.md b/.changeset/nine-monkeys-serve.md new file mode 100644 index 0000000..c550174 --- /dev/null +++ b/.changeset/nine-monkeys-serve.md @@ -0,0 +1,5 @@ +--- +"@codee-sh/medusa-plugin-automations": patch +--- + +feat: Add throttle support for event triggers diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index e79049d..1d15336 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -5,9 +5,17 @@ name: Generate Release Notes (Backup) +# DISABLED - Using GitHub's built-in "Generate release notes" instead +# This workflow is kept for reference but will NOT run automatically. +# To enable: uncomment the "on:" section below and remove workflow_dispatch + +# on: +# release: +# types: [created] + +# Only allows manual trigger, prevents automatic execution on: - release: - types: [created] + workflow_dispatch: jobs: generate-release-notes: diff --git a/.gitignore b/.gitignore index 0839fea..8e2db68 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ yarn-error.log* .env .env.local +# Temporary docs (internal) +docs-temp/ + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0a589cf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,437 @@ +# Contributing + +Thank you for your interest in contributing to this project! This document describes the workflow for working on the repository and the conventions we should follow. If you have any questions, we encourage you to reach out via issues or directly with the team. + +## Important Information + +- All changes should be made through Pull Requests (PR) +- Before starting work on larger changes, consult with the team +- All PRs should include appropriate tests +- Code should follow the accepted style conventions +- **The project uses Changesets** for version management - every PR with code changes requires a changeset + +## Prerequisites + +- Knowledge of Git and GitHub (Issues, Pull Requests) +- Local development environment configured +- Node.js >= 20 +- Yarn 3.2.3+ +- Knowledge of technologies used in the project (Medusa, TypeScript, React) + +## Workflow + +### 1. Issues before PR + +1. **Check existing issues** - before starting work, make sure there isn't already an issue for what you want to work on +2. **Create an issue** - if there's no appropriate issue, create a new one, describing: + - What you want to implement/change + - Why this change is needed + - What are the expected results +3. **Wait for approval** - for larger changes, wait for team approval before starting work + +### 2. Branches + +All changes should be made in separate branches and submitted as Pull Requests. + +**Branch naming:** +- `fix/` - for bug fixes (e.g., `fix/login-error`) +- `feat/` - for new features (e.g., `feat/user-profile`) +- `docs/` - for documentation changes (e.g., `docs/api-update`) +- `refactor/` - for code refactoring (e.g., `refactor/auth-module`) +- `test/` - for adding or improving tests (e.g., `test/integration-tests`) +- `chore/` - for maintenance tasks (e.g., `chore/dependencies-update`) + +**Base branch:** +- Use `develop` as the base branch for your PRs by default +- `master` is only used by admins for releases + +### 3. Commits + +- Try to create small, isolated commits - this makes review and understanding changes easier +- Use conventional commit messages: + - `fix: fixed login error` + - `feat: added password reset functionality` + - `docs: updated API documentation` + - `refactor: improved authorization module structure` + - `test: added unit tests for payment module` + - `chore: updated dependencies` + +### 4. Pull Requests + +**Process (for developers):** +1. Make sure your branch is up to date with `develop` +2. **Add a changeset** (if you're making code changes) - see the [Changesets](#changesets) section +3. Create a Pull Request with a clear description of changes **to the `develop` branch** +4. Add appropriate labels and assign reviewers +5. Respond to comments and make corrections +6. After approval, the PR will be merged into `develop` + +**Release process (admins only):** + +1. After merging changes to `develop`, admin creates a PR from `develop` to `master` +2. This PR contains all changes ready for release +3. After merging to `master`, admin performs release locally (see the [Release Process](#release-process-locally-by-admin) section) + +**How to create a PR from `develop` to `master`:** + +1. **Make sure `develop` is up to date:** +```bash +git checkout develop +git pull origin develop +``` + +2. **Create PR on GitHub:** + - Base branch: `master` + - Compare branch: `develop` + - PR title: `chore: release [date]` or `chore: prepare release` (e.g., `chore: release 2024-01-15`) + +3. **In the PR description you can:** + - List the main changes in this release + - Add links to merged PRs from `develop` + - Optionally: add a list of changesets that will be processed + +4. **After creating the PR:** + - Merge the PR to `master` (squash merge or merge commit) + - After merge, perform the release process locally (see the [Release Process](#release-process-locally-by-admin) section) + +**PR description structure:** +- **What** - what was changed in this PR +- **Why** - why these changes are needed +- **How** - how the changes were implemented +- **Testing** - how the changes were tested or how the reviewer can test them +- **Related issue** - link to the related issue (use keywords: `closes #123`, `fixes #456`) + +**Self-review:** +We encourage self-review of code before requesting review. Check: +- Is the code readable and follows conventions +- Do all tests pass +- Are there no unused imports or comments +- Has documentation been updated (if applicable) +- Has a changeset been added (if applicable) + +**Merge Style:** +- Pull Requests are merged via squash and merge +- Make sure the commit message is clear and descriptive + +## Local Development + +### Environment Setup + +1. **Clone the repository:** +```bash +git clone +cd +``` + +2. **Install dependencies:** +```bash +yarn install +``` + +3. **Start the development server:** +```bash +yarn dev +``` + +This will start the plugin in watch mode, automatically rebuilding on changes. + +### Project Structure + +``` +medusa-plugin-automations/ +├── src/ +│ ├── admin/ # Admin UI (React components) +│ ├── api/ # API routes (admin/store) +│ ├── modules/ # Medusa modules +│ ├── workflows/ # Medusa workflows +│ ├── subscribers/ # Event subscribers +│ ├── providers/ # Action providers (Slack, etc.) +│ └── utils/ # Utility functions +├── docs/ # Documentation +├── .changeset/ # Changesets for versioning +├── .github/ # GitHub workflows +└── package.json +``` + +### Working with the plugin locally (yalc) + +If you're working on the plugin and want to test it in a Medusa application: + +1. **In the plugin directory, build and publish locally:** +```bash +yarn build +yalc publish +``` + +2. **In the Medusa application, link the local plugin:** +```bash +cd /path/to/medusa-app +yalc link @codee-sh/medusa-plugin-automations +yarn install +``` + +3. **While working on the plugin:** +```bash +# In the plugin directory (watch mode) +yarn dev + +# In the Medusa application (in a separate terminal) +yarn dev +``` + +4. **After finishing work, remove local links:** +```bash +cd /path/to/medusa-app +yalc remove @codee-sh/medusa-plugin-automations +yarn install +``` + +**Note:** `.yalc` and `yalc.lock` files are ignored by git - don't commit them. + +## Testing + +### Types of Tests + +- **Unit tests** - unit tests for individual functions/modules +- **Integration tests** - integration tests for larger components +- **E2E tests** - end-to-end tests for entire flows + +### Running Tests + +```bash +# All tests +yarn test + +# Tests in watch mode +yarn test:watch + +# Tests with coverage +yarn test:coverage + +# Integration tests +yarn test:integration +``` + +### Test Requirements + +- All PRs should include appropriate tests for the changes made +- New features require new tests +- Bug fixes should include tests that reproduce and verify the fix +- Aim for code coverage >= 80% + +## Documentation + +### Updating Documentation + +- If you change user-facing API, update documentation in `docs/` +- Add usage examples for new features +- Update README.md if you change the setup or installation process +- Document breaking changes through changesets (CHANGELOG.md is generated automatically) + +### Documentation Conventions + +- Use [TSDoc](https://tsdoc.org/) for TypeScript documentation +- Use JSDoc for JavaScript documentation +- Write documentation in English (or according to project convention) +- Add code examples where possible + +## Code Style + +### Formatting + +- We use Prettier for code formatting +- Run `yarn format` before committing (formats files) +- Check formatting before PR: `yarn format:check` (checks without formatting) + +### Linting + +- All files should pass linting without errors +- Run `yarn format:check` before committing (checks formatting) +- Fix all warnings before PR + +### TypeScript + +- Use TypeScript for all new files +- Avoid `any` - use appropriate types +- Add types for all public APIs +- Use interfaces for objects, type for union types +- Export types from `src/utils/types/` for public API + +## Changesets + +The project uses [Changesets](https://github.com/changesets/changesets) for version and changelog management. + +### Changesets Workflow + +The release process consists of **three stages**: + +1. **Feature PR** (you create) - contains code changes + changeset file → merge to `develop` +2. **Release PR** (admin creates) - PR from `develop` to `master` with ready changes +3. **Manual release** (admin executes locally) - after merge to `master`, admin locally runs `yarn version` and `yarn release` + +**Flow diagram:** +``` +Developer: + feature-branch → PR → develop (with changeset) + +Admin: + develop → PR "chore: release [date]" → master + +Admin (locally, after merge to master): + 1. git checkout master && git pull + 2. yarn version (updates version and CHANGELOG) + 3. git commit && git push + 4. yarn release (builds and publishes to npm) +``` + +### How to Add a Changeset + +1. **After making code changes, before creating PR:** +```bash +yarn changeset +``` + +2. **Select the type of change:** + - `major` - breaking changes + - `minor` - new features (backward compatible) + - `patch` - bug fixes (backward compatible) + +3. **Describe the changes** - write a brief description of what was changed + +4. **Commit the changeset file:** +```bash +git add .changeset/ +git commit -m "feat: add changeset for my feature" +``` + +5. **Create PR to `develop`** - make sure the changeset file is included in the PR + +### Release Process (locally by admin) + +After merging PR from `develop` to `master`: + +```bash +# 1. Switch to master and pull changes +git checkout master && git pull origin master + +# 2. Update version and CHANGELOG +yarn version + +# 3. Commit and push changes +git add package.json CHANGELOG.md .changeset/ +git commit -m "chore: version packages" +git push origin master + +# 4. Build and publish to npm +yarn release + +# 5. Push tag +git push origin --tags +``` + +**Important:** +- Changesets must be in PR to `develop` - without them `yarn version` won't find changes +- Make sure you're logged in to npm (`npm login`) before `yarn release` + +### Changesets Commands + +#### `yarn changeset` + +**What it does:** Creates a new changeset file describing code changes. + +**When to use:** +- After making code changes, **before creating PR** +- For every code change that should be included in the release +- **Don't use** for documentation-only changes (unless it's a breaking change in documentation) + +**How to use:** +```bash +yarn changeset +``` + +**Process:** Select the type of change (major/minor/patch) and describe the changes. A file will be created in `.changeset/`. + +--- + +#### `yarn version` + +**What it does:** +- Reads all changesets in `.changeset/` +- Updates version in `package.json` according to changeset types +- Updates `CHANGELOG.md` with change descriptions +- Removes processed changeset files + +**When to use:** +- **Admins only** after merging PR `develop` → `master`, on `master` branch +- Before `yarn release` + +**How to use:** +```bash +yarn version +``` + +**Process:** Checks changesets, updates version in `package.json` and `CHANGELOG.md`, removes processed changeset files. + +**After running:** Commit the changes (`package.json`, `CHANGELOG.md`, `.changeset/`). + +--- + +#### `yarn release` + +**What it does:** +- Builds the package (`yarn build`) +- Publishes the package to npm (`npm publish`) +- Creates a git tag with the version + +**When to use:** +- **Admins only** after `yarn version` and committing changes +- On `master` branch with updated version + +**How to use:** +```bash +yarn release +``` + +**Process:** Builds the package (`yarn build`), publishes to npm (`yarn publish-package`), creates a git tag. + +**Requirements:** Log in to npm (`npm login`), changes must be committed and pushed. + +--- + +**Versioning:** The project uses [Semantic Versioning](https://semver.org/): +- `major` - breaking changes +- `minor` - new features (backward compatible) +- `patch` - bug fixes (backward compatible) + +CHANGELOG.md is automatically generated by Changesets. + +## Code Review + +### For PR Authors + +- Be open to feedback +- Respond to all comments +- Make corrections according to suggestions +- If you disagree with a suggestion, explain why + +### For Reviewers + +- Be constructive and polite +- Explain your suggestions +- Check not only code, but also tests and documentation +- Check if a changeset has been added (if applicable) +- Approve PR only if you're sure it's ready + +## Questions and Help + +- **Issues** - for bugs and feature requests +- **Discussions** - for questions and discussions +- **Team** - direct contact with team members + +## License + +By contributing to this project, you agree that your changes will be licensed under the same terms as the project. + +--- + +Thank you for your contribution! 🎉 diff --git a/package.json b/package.json index ecac960..da3ea1f 100755 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@changesets/cli": "^2.29.8", - "@codee-sh/medusa-plugin-notification-emails": "0.0.2", + "@codee-sh/medusa-plugin-notification-emails": "0.1.0", "@medusajs/admin-sdk": "2.8.8", "@medusajs/cli": "2.8.8", "@medusajs/framework": "2.8.8", diff --git a/src/admin/automations/automations-form/automations-create-form/index.tsx b/src/admin/automations/automations-form/automations-create-form/index.tsx index 7f25536..ed1f1a7 100644 --- a/src/admin/automations/automations-form/automations-create-form/index.tsx +++ b/src/admin/automations/automations-form/automations-create-form/index.tsx @@ -78,7 +78,7 @@ export function AutomationsCreateForm() { description: "", trigger_type: "event", event_name: "", - interval_minutes: null, + interval_seconds: null, active: false, }, rules: { @@ -104,8 +104,8 @@ export function AutomationsCreateForm() { // description: trigger.description || "", // trigger_type: trigger.trigger_type || "event", // event_name: trigger.event_name || "", - // interval_minutes: - // trigger.interval_minutes || null, + // interval_seconds: + // trigger.interval_seconds || null, // active: trigger.active || false, // }, // rules: { @@ -126,7 +126,7 @@ export function AutomationsCreateForm() { description: "", trigger_type: "event", event_name: "", - interval_minutes: null, + interval_seconds: null, active: false, }, rules: { @@ -146,7 +146,7 @@ export function AutomationsCreateForm() { description: data.general.description, trigger_type: data.general.trigger_type, event_name: data.general.event_name, - interval_minutes: data.general.interval_minutes, + interval_seconds: data.general.interval_seconds, active: data.general.active, } diff --git a/src/admin/automations/automations-form/automations-edit-form/index.tsx b/src/admin/automations/automations-form/automations-edit-form/index.tsx index 6865928..827ac80 100644 --- a/src/admin/automations/automations-form/automations-edit-form/index.tsx +++ b/src/admin/automations/automations-form/automations-edit-form/index.tsx @@ -124,7 +124,7 @@ export function AutomationsEditForm({ description: "", trigger_type: "event", event_name: "", - interval_minutes: null, + interval_seconds: null, active: false, }, rules: { @@ -154,8 +154,8 @@ export function AutomationsEditForm({ description: trigger.description || "", trigger_type: trigger.trigger_type || "event", event_name: trigger.event_name || "", - interval_minutes: - trigger.interval_minutes || null, + interval_seconds: + trigger.interval_seconds || null, active: trigger.active || false, }, rules: { @@ -202,7 +202,7 @@ export function AutomationsEditForm({ description: "", trigger_type: "event", event_name: "", - interval_minutes: null, + interval_seconds: null, active: false, }, rules: { @@ -223,7 +223,7 @@ export function AutomationsEditForm({ description: data.general.description, trigger_type: data.general.trigger_type, event_name: data.general.event_name, - interval_minutes: data.general.interval_minutes, + interval_seconds: data.general.interval_seconds, active: data.general.active, } diff --git a/src/admin/automations/automations-form/automations-general-form/index.tsx b/src/admin/automations/automations-form/automations-general-form/index.tsx index a6351b3..b8685a0 100644 --- a/src/admin/automations/automations-form/automations-general-form/index.tsx +++ b/src/admin/automations/automations-form/automations-general-form/index.tsx @@ -3,11 +3,12 @@ import { Label, Select, Checkbox, + Text, } from "@medusajs/ui" import { useAvailableEvents } from "../../../../hooks/api/available-events" import { useAvailableTriggers } from "../../../../hooks/api/available-triggers" import { useAvailableActions } from "../../../../hooks/api/available-actions" -import { Controller } from "react-hook-form" +import { Controller, useWatch } from "react-hook-form" import { useMemo } from "react" export function AutomationsGeneralForm({ @@ -52,6 +53,16 @@ export function AutomationsGeneralForm({ return availableActionsData?.actions || [] }, [availableActionsData]) + // Watch trigger_type to show/hide interval_seconds field + const triggerType = useWatch({ + control: form.control, + name: "general.trigger_type", + }) + + // Show interval field for event and schedule types + const showIntervalField = + triggerType === "event" || triggerType === "schedule" + return (
@@ -223,6 +234,58 @@ export function AutomationsGeneralForm({ )} />
+ {showIntervalField && ( +
+ + ( + <> + { + const value = e.target.value + field.onChange( + value === "" + ? null + : Number(value) + ) + }} + /> + + {triggerType === "schedule" + ? "How often to run this automation" + : "Optional: Limit how often this automation can run for the same target (e.g., 3600 = max once per hour)"} + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+ )}
diff --git a/src/admin/automations/automations-form/automations-rules-form/index.tsx b/src/admin/automations/automations-form/automations-rules-form/index.tsx index 48a8fa1..9e592d7 100644 --- a/src/admin/automations/automations-form/automations-rules-form/index.tsx +++ b/src/admin/automations/automations-form/automations-rules-form/index.tsx @@ -1,10 +1,9 @@ -import { Label, Select, Button } from "@medusajs/ui" +import { Button } from "@medusajs/ui" import { useAvailableEvents } from "../../../../hooks/api/available-events" -import { OPERATOR_TYPES } from "../../../../modules/mpn-automation/types/types" -import { Controller, useFieldArray } from "react-hook-form" +import { useFieldArray } from "react-hook-form" import { useMemo } from "react" -import { Trash, Plus } from "@medusajs/icons" -import { RuleValueInput } from "./rule-value-input" +import { Plus } from "@medusajs/icons" +import { RuleItem } from "./rule-item" export function AutomationsRulesForm({ form, @@ -76,116 +75,13 @@ export function AutomationsRulesForm({ )} {fields.map((field, index) => ( -
-
-
- ( - <> - - - {fieldState.error && ( - - {fieldState.error.message} - - )} - - )} - /> - ( - <> - - - {fieldState.error && ( - - {fieldState.error.message} - - )} - - )} - /> - -
- -
-
+ control={form.control} + index={index} + eventAttributes={eventAttributes} + onRemove={() => handleRemoveRule(index)} + /> ))} + + + ) +} + diff --git a/src/admin/automations/automations-form/types/schema.ts b/src/admin/automations/automations-form/types/schema.ts index 2e6dc6a..cdba324 100644 --- a/src/admin/automations/automations-form/types/schema.ts +++ b/src/admin/automations/automations-form/types/schema.ts @@ -13,7 +13,7 @@ export const baseAutomationFormSchema = z.object({ .min(3, "Description must be at least 3 characters"), trigger_type: z.enum(["event", "schedule", "manual"]), event_name: z.string().min(1, "Event name is required"), - interval_minutes: z.number().nullable(), + interval_seconds: z.number().nullable(), active: z.boolean(), }), rules: z diff --git a/src/admin/automations/automations-list/automations-list.tsx b/src/admin/automations/automations-list/automations-list.tsx index c71cccc..3280ab7 100644 --- a/src/admin/automations/automations-list/automations-list.tsx +++ b/src/admin/automations/automations-list/automations-list.tsx @@ -50,7 +50,7 @@ export const AutomationsList = () => { const columns = useMemo( () => [ columnHelper.accessor("to", { - header: "Name and description", + header: "Name and descriptions", cell: ({ row }) => { const tooltip = `Device (DB) ID: \n ${row?.original?.id}` return ( @@ -71,7 +71,7 @@ export const AutomationsList = () => { -
+
{row?.original?.description}
diff --git a/src/api/middlewares.ts b/src/api/middlewares.ts index 6393186..367d303 100644 --- a/src/api/middlewares.ts +++ b/src/api/middlewares.ts @@ -128,7 +128,7 @@ export default defineMiddlewares({ "trigger_id", "trigger_type", "event_name", - "interval_minutes", + "interval_seconds", "channels", "metadata", "active", diff --git a/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json b/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json index b90e4a3..4bf5b2c 100644 --- a/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json +++ b/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json @@ -56,8 +56,8 @@ "nullable": true, "mappedType": "text" }, - "interval_minutes": { - "name": "interval_minutes", + "interval_seconds": { + "name": "interval_seconds", "type": "integer", "unsigned": false, "autoincrement": false, diff --git a/src/modules/mpn-automation/migrations/Migration20251222121805.ts b/src/modules/mpn-automation/migrations/Migration20251222121805.ts new file mode 100644 index 0000000..6f3bbcf --- /dev/null +++ b/src/modules/mpn-automation/migrations/Migration20251222121805.ts @@ -0,0 +1,15 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20251222121805 extends Migration { + override async up(): Promise { + this.addSql( + `alter table if exists "mpn_automation_trigger" rename column "interval_minutes" to "interval_seconds";` + ) + } + + override async down(): Promise { + this.addSql( + `alter table if exists "mpn_automation_trigger" rename column "interval_seconds" to "interval_minutes";` + ) + } +} diff --git a/src/modules/mpn-automation/models/mpn_automation_trigger.ts b/src/modules/mpn-automation/models/mpn_automation_trigger.ts index 9f59ee4..e8339ac 100644 --- a/src/modules/mpn-automation/models/mpn_automation_trigger.ts +++ b/src/modules/mpn-automation/models/mpn_automation_trigger.ts @@ -23,8 +23,8 @@ export const MpnAutomationTrigger = model // Event name — only when trigger_type = "event" event_name: model.text().nullable(), - // Interval in minutes — only when trigger_type = "schedule" - interval_minutes: model.number().nullable(), + // Interval in seconds — only when trigger_type = "schedule" + interval_seconds: model.number().nullable(), // Whether the trigger is enabled active: model.boolean().default(true), diff --git a/src/modules/mpn-automation/services/slack-action-service.ts b/src/modules/mpn-automation/services/slack-action-service.ts index 9ad8566..6e1f592 100644 --- a/src/modules/mpn-automation/services/slack-action-service.ts +++ b/src/modules/mpn-automation/services/slack-action-service.ts @@ -6,6 +6,11 @@ import { import { renderInventoryLevel } from "../../../templates/slack/inventory-level" import { renderProductVariant } from "../../../templates/slack/product-variant/product-variant" import { renderProduct } from "../../../templates/slack/product/product" +import { renderOrderPlaced } from "../../../templates/slack/order/order-placed" +import { renderOrderCompleted } from "../../../templates/slack/order/order-completed" +import { renderOrderUpdated } from "../../../templates/slack/order/order-updated" +import { renderOrderCanceled } from "../../../templates/slack/order/order-canceled" +import { renderOrderArchived } from "../../../templates/slack/order/order-archived" export class SlackActionService extends BaseActionService { id = "slack" @@ -35,6 +40,12 @@ export class SlackActionService extends BaseActionService { renderProductVariant as any ) this.registerTemplate("product", renderProduct as any) + + this.registerTemplate("order-placed", renderOrderPlaced as any) + this.registerTemplate("order-completed", renderOrderCompleted as any) + this.registerTemplate("order-updated", renderOrderUpdated as any) + this.registerTemplate("order-canceled", renderOrderCanceled as any) + this.registerTemplate("order-archived", renderOrderArchived as any) } /** diff --git a/src/modules/mpn-automation/types/interfaces.ts b/src/modules/mpn-automation/types/interfaces.ts index bd97c67..6cda1cd 100644 --- a/src/modules/mpn-automation/types/interfaces.ts +++ b/src/modules/mpn-automation/types/interfaces.ts @@ -29,7 +29,7 @@ export interface AutomationTrigger { description: string | null trigger_type: TriggerType event_name: string | null - interval_minutes: number | null + interval_seconds: number | null active: boolean channels: Record | null metadata: Record | null diff --git a/src/modules/mpn-automation/types/modules/index.ts b/src/modules/mpn-automation/types/modules/index.ts index b661c8a..a2db6fa 100644 --- a/src/modules/mpn-automation/types/modules/index.ts +++ b/src/modules/mpn-automation/types/modules/index.ts @@ -7,6 +7,7 @@ import { PRODUCT_VARIANT_ATTRIBUTES } from "./product-variant" import { PRODUCT_TAG_ATTRIBUTES } from "./product-tag" import { PRODUCT_TYPE_ATTRIBUTES } from "./product-type" import { PRODUCT_CATEGORY_ATTRIBUTES } from "./product-category" +import { ORDER_ATTRIBUTES } from "./order" import { Attribute } from "../types" /** @@ -14,6 +15,16 @@ import { Attribute } from "../types" */ export type EventMetadata = { eventName: string + /** + * Description of when this event is triggered + * Example: "Triggered when a customer completes checkout and an order is created" + */ + description?: string + /** + * Example scenarios when this event would fire + * Example: ["Customer completes payment", "Order is confirmed"] + */ + examples?: string[] attributes: Array templates: Array<{ value: string; name: string }> } @@ -38,6 +49,12 @@ export function getEventMetadata( const EVENT_METADATA_REGISTRY: Record = { // Inventory Events "inventory.inventory-level.created": { + description: "Triggered when a new inventory level is created for a location", + examples: [ + "New stock location is added", + "Inventory item is assigned to a location", + "Initial stock is recorded" + ], attributes: INVENTORY_LEVEL_ATTRIBUTES, templates: [ { @@ -47,6 +64,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "inventory.inventory-level.updated": { + description: "Triggered when inventory level changes (stock quantity, reserved quantity, etc.)", + examples: [ + "Stock quantity is updated", + "Items are reserved or released", + "Inventory adjustments are made" + ], attributes: INVENTORY_LEVEL_ATTRIBUTES, templates: [ { @@ -56,6 +79,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "inventory.inventory-level.deleted": { + description: "Triggered when an inventory level is deleted from a location", + examples: [ + "Stock location is removed", + "Inventory item is unassigned from a location", + "Inventory level record is deleted" + ], attributes: INVENTORY_LEVEL_ATTRIBUTES, templates: [ { @@ -65,6 +94,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "inventory.inventory-item.created": { + description: "Triggered when a new inventory item is created", + examples: [ + "New product variant is added to inventory", + "Inventory item is registered in the system", + "Stock tracking begins for a new item" + ], attributes: INVENTORY_ITEM_ATTRIBUTES, templates: [ { @@ -74,6 +109,13 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "inventory.inventory-item.updated": { + description: "Triggered when inventory item data is modified (quantities, location, etc.)", + examples: [ + "Stock quantity changes", + "Reserved quantity is updated", + "Available quantity changes", + "Incoming quantity is adjusted" + ], attributes: INVENTORY_ITEM_ATTRIBUTES, templates: [ { @@ -83,6 +125,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "inventory.inventory-item.deleted": { + description: "Triggered when an inventory item is deleted", + examples: [ + "Product variant is removed from inventory", + "Inventory item is discontinued", + "Stock tracking is stopped for an item" + ], attributes: INVENTORY_ITEM_ATTRIBUTES, templates: [ { @@ -92,6 +140,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "product.updated": { + description: "Triggered when product data is modified (title, description, status, etc.)", + examples: [ + "Product title or description changes", + "Product status is updated", + "Product metadata is modified" + ], attributes: PRODUCT_ATTRIBUTES, templates: [ { @@ -101,6 +155,13 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "product-variant.updated": { + description: "Triggered when product variant data is modified (SKU, price, inventory settings, etc.)", + examples: [ + "Variant SKU is updated", + "Variant price changes", + "Inventory management settings change", + "Variant attributes are modified" + ], attributes: PRODUCT_VARIANT_ATTRIBUTES, templates: [ { @@ -110,6 +171,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "product-tag.updated": { + description: "Triggered when a product tag is modified", + examples: [ + "Tag name/value is changed", + "Tag is renamed", + "Tag metadata is updated" + ], attributes: PRODUCT_TAG_ATTRIBUTES, templates: [ { @@ -119,6 +186,12 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "product-type.updated": { + description: "Triggered when a product type is modified", + examples: [ + "Product type name/value is changed", + "Product type is renamed", + "Product type metadata is updated" + ], attributes: PRODUCT_TYPE_ATTRIBUTES, templates: [ { @@ -128,6 +201,13 @@ const EVENT_METADATA_REGISTRY: Record = { ], }, "product-category.updated": { + description: "Triggered when a product category is modified (name, description, parent, etc.)", + examples: [ + "Category name or description changes", + "Category parent is changed", + "Category status (active/inactive) is updated", + "Category rank/order is modified" + ], attributes: PRODUCT_CATEGORY_ATTRIBUTES, templates: [ { @@ -136,4 +216,81 @@ const EVENT_METADATA_REGISTRY: Record = { }, ], }, + "order.updated": { + description: "Triggered when any order data is modified (status, totals, items, etc.)", + examples: [ + "Order status changes", + "Order totals are recalculated", + "Items are added or removed", + "Payment collection status changes" + ], + attributes: ORDER_ATTRIBUTES, + templates: [ + { + value: "order-updated", + name: "Order updated", + }, + ], + }, + "order.placed": { + description: "Triggered when a customer completes checkout and an order is created", + examples: [ + "Customer completes payment", + "Order is confirmed", + "Order enters the system" + ], + attributes: ORDER_ATTRIBUTES, + templates: [ + { + value: "order-placed", + name: "Order placed", + }, + ], + }, + "order.canceled": { + description: "Triggered when an order is canceled", + examples: [ + "Customer cancels their order", + "Merchant cancels an order", + "Order is canceled due to payment failure", + "Order cancellation is processed" + ], + attributes: ORDER_ATTRIBUTES, + templates: [ + { + value: "order-canceled", + name: "Order canceled", + }, + ], + }, + "order.completed": { + description: "Triggered when an order is marked as completed", + examples: [ + "All items are fulfilled", + "Order is finalized", + "Order processing is finished" + ], + attributes: ORDER_ATTRIBUTES, + templates: [ + { + value: "order-completed", + name: "Order completed", + }, + ], + }, + "order.archived": { + description: "Triggered when an order is archived", + examples: [ + "Order is moved to archive", + "Completed order is archived", + "Old order is archived for record keeping" + ], + attributes: ORDER_ATTRIBUTES, + templates: [ + { + value: "order-archived", + name: "Order archived", + }, + ], + }, } diff --git a/src/modules/mpn-automation/types/modules/inventory/inventory.ts b/src/modules/mpn-automation/types/modules/inventory/inventory.ts index ee19a31..b9101a8 100644 --- a/src/modules/mpn-automation/types/modules/inventory/inventory.ts +++ b/src/modules/mpn-automation/types/modules/inventory/inventory.ts @@ -2,22 +2,32 @@ export const INVENTORY_ITEM_ATTRIBUTES = [ { value: "inventory_item.stocked_quantity", label: "Stocked Quantity", + description: "Total quantity of items in stock", + examples: ["0", "10", "100", "500"], }, { value: "inventory_item.reserved_quantity", label: "Reserved Quantity", + description: "Quantity of items currently reserved (e.g., in carts or pending orders)", + examples: ["0", "5", "20", "50"], }, { value: "inventory_item.available_quantity", label: "Available Quantity", + description: "Quantity available for sale (stocked - reserved)", + examples: ["0", "5", "50", "200"], }, { value: "inventory_item.incoming_quantity", label: "Incoming Quantity", + description: "Quantity of items expected to arrive (e.g., from suppliers)", + examples: ["0", "10", "100", "500"], }, { value: "inventory_item.location_id", label: "Location ID", + description: "Unique identifier of the inventory location", + examples: ["loc_01ABC123"], }, ] @@ -25,34 +35,50 @@ export const INVENTORY_LEVEL_ATTRIBUTES = [ { value: "inventory_level.id", label: "ID", + description: "Unique identifier of the inventory level", + examples: ["ilev_01ABC123", "ilev_01XYZ789"], }, { value: "inventory_level.inventory_item_id", label: "Inventory Item ID", + description: "Unique identifier of the inventory item", + examples: ["iitem_01ABC123"], }, { value: "inventory_level.stocked_quantity", label: "Stocked Quantity", + description: "Total quantity of items in stock at this location", + examples: ["0", "10", "100", "500"], }, { value: "inventory_level.reserved_quantity", label: "Reserved Quantity", + description: "Quantity of items currently reserved at this location", + examples: ["0", "5", "20", "50"], }, { value: "inventory_level.available_quantity", label: "Available Quantity", + description: "Quantity available for sale at this location (stocked - reserved)", + examples: ["0", "5", "50", "200"], }, { value: "inventory_level.incoming_quantity", label: "Incoming Quantity", + description: "Quantity of items expected to arrive at this location", + examples: ["0", "10", "100", "500"], }, { value: "inventory_level.location_id", label: "Location ID", + description: "Unique identifier of the stock location", + examples: ["loc_01ABC123"], }, { value: "inventory_level.stock_locations.id", label: "Stock Location ID", + description: "Unique identifier of the stock location. This is an array - operator 'eq' checks if ANY value matches", + examples: ["loc_01ABC123"], type: "array", isRelation: true, relationType: "stock_locations", @@ -60,8 +86,22 @@ export const INVENTORY_LEVEL_ATTRIBUTES = [ { value: "inventory_level.stock_locations.name", label: "Stock Location Name", + description: "Name of the stock location. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Main Warehouse", "Store A", "Distribution Center"], type: "array", isRelation: true, relationType: "stock_locations", }, ] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are needed for correct data retrieval including all relation data +// INVENTORY_LEVEL_QUERY_FIELDS contains all fields from INVENTORY_LEVEL_ATTRIBUTES plus technical relations +export const INVENTORY_LEVEL_QUERY_FIELDS = [ + // Basic fields from INVENTORY_LEVEL_ATTRIBUTES + ...INVENTORY_LEVEL_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for complete data retrieval + // These fields are not available in UI rules, but are needed for correct data retrieval + "inventory_level.stock_locations.*", +] diff --git a/src/modules/mpn-automation/types/modules/order/helpers.ts b/src/modules/mpn-automation/types/modules/order/helpers.ts new file mode 100644 index 0000000..5e45cf2 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/order/helpers.ts @@ -0,0 +1,30 @@ +import { OrderStatus, PaymentCollectionStatus } from "@medusajs/framework/utils" + +/** + * Helper to get all possible OrderStatus values + */ +export const ORDER_STATUS_VALUES = Object.values(OrderStatus) as string[] + +/** + * Helper to get all possible PaymentCollectionStatus values + */ +export const PAYMENT_COLLECTION_STATUS_VALUES = Object.values( + PaymentCollectionStatus +) as string[] + +/** + * FulfillmentStatus is a union type, not an enum, so we define values manually + * Based on: @medusajs/framework/types - FulfillmentStatus + * import { FulfillmentStatus } from "@medusajs/framework/types" + */ +export const FULFILLMENT_STATUS_VALUES: string[] = [ + "not_fulfilled", + "partially_fulfilled", + "fulfilled", + "partially_shipped", + "shipped", + "partially_delivered", + "delivered", + "canceled", +] + diff --git a/src/modules/mpn-automation/types/modules/order/index.ts b/src/modules/mpn-automation/types/modules/order/index.ts new file mode 100644 index 0000000..1227c68 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/order/index.ts @@ -0,0 +1 @@ +export * from "./order" diff --git a/src/modules/mpn-automation/types/modules/order/order.ts b/src/modules/mpn-automation/types/modules/order/order.ts new file mode 100644 index 0000000..d1caf38 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/order/order.ts @@ -0,0 +1,563 @@ +import { + ORDER_STATUS_VALUES, + PAYMENT_COLLECTION_STATUS_VALUES, + FULFILLMENT_STATUS_VALUES, +} from "./helpers" + +// Attributes available in rules (without technical relations with *) +// These attributes are displayed in the UI for creating conditions in automations +export const ORDER_ATTRIBUTES = [ + // Basic fields + { + value: "order.id", + label: "ID", + description: "Unique identifier of the order", + examples: ["order_01ABC123", "order_01XYZ789"], + }, + { + value: "order.display_id", + label: "Display ID", + description: "Human-readable order number displayed to customers", + examples: ["#1001", "#2050", "#9999"], + }, + { + value: "order.custom_display_id", + label: "Custom Display ID", + description: "Custom order identifier set by the merchant", + examples: ["CUSTOM-001", "PO-2024-001"], + }, + { + value: "order.status", + label: "Status", + description: "Current status of the order", + examples: ORDER_STATUS_VALUES, + }, + { + value: "order.locale", + label: "Locale", + description: "Locale code for the order (language and region)", + examples: ["pl", "en", "en-US", "pl-PL"], + }, + { + value: "order.email", + label: "Email", + description: "Customer email address associated with the order", + examples: ["customer@example.com", "user@domain.com"], + }, + { + value: "order.currency_code", + label: "Currency Code", + description: "ISO 4217 currency code for the order", + examples: ["USD", "EUR", "PLN", "GBP"], + }, + { + value: "order.region_id", + label: "Region ID", + description: "Unique identifier of the region", + examples: ["reg_01ABC123"], + }, + { + value: "order.created_at", + label: "Created At", + description: "Date and time when the order was created (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], + }, + { + value: "order.updated_at", + label: "Updated At", + description: "Date and time when the order was last updated (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], + }, + // Totals + { + value: "order.total", + label: "Total", + description: "Total amount of the order including taxes, shipping, and discounts", + examples: ["100.00", "250.50", "999.99"], + }, + { + value: "order.subtotal", + label: "Subtotal", + description: "Subtotal amount before taxes and shipping", + examples: ["80.00", "200.00", "850.00"], + }, + { + value: "order.tax_total", + label: "Tax Total", + description: "Total amount of taxes applied to the order", + examples: ["20.00", "50.00", "149.99"], + }, + { + value: "order.original_total", + label: "Original Total", + description: "Original total amount before any changes or adjustments", + examples: ["100.00", "250.50", "999.99"], + }, + { + value: "order.original_subtotal", + label: "Original Subtotal", + description: "Original subtotal before any changes or adjustments", + examples: ["80.00", "200.00", "850.00"], + }, + { + value: "order.original_tax_total", + label: "Original Tax Total", + description: "Original tax total before any changes or adjustments", + examples: ["20.00", "50.00", "149.99"], + }, + { + value: "order.discount_total", + label: "Discount Total", + description: "Total amount of discounts applied to the order", + examples: ["0.00", "10.00", "50.00"], + }, + { + value: "order.discount_tax_total", + label: "Discount Tax Total", + description: "Tax amount on discounts applied to the order", + examples: ["0.00", "2.50", "12.50"], + }, + // Shipping (specific fields, not *) + { + value: "order.shipping_methods.amount", + label: "Shipping Methods Amount", + description: "Total shipping cost for all shipping methods. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "10.00", "25.50"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.subtotal", + label: "Shipping Methods Subtotal", + description: "Shipping subtotal before taxes. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "8.00", "20.00"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.tax_total", + label: "Shipping Methods Tax Total", + description: "Tax amount on shipping. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "2.00", "5.50"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.original_total", + label: "Shipping Methods Original Total", + description: "Original shipping total before adjustments. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "10.00", "25.50"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.original_subtotal", + label: "Shipping Methods Original Subtotal", + description: "Original shipping subtotal before adjustments. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "8.00", "20.00"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.original_tax_total", + label: "Shipping Methods Original Tax Total", + description: "Original shipping tax total before adjustments. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "2.00", "5.50"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.discount_total", + label: "Shipping Methods Discount Total", + description: "Total shipping discounts applied. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "5.00", "10.00"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.discount_subtotal", + label: "Shipping Methods Discount Subtotal", + description: "Shipping discount subtotal. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "4.00", "8.00"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + { + value: "order.shipping_methods.discount_tax_total", + label: "Shipping Methods Discount Tax Total", + description: "Tax amount on shipping discounts. This is an array - operator 'eq' checks if ANY value matches", + examples: ["0.00", "1.00", "2.00"], + type: "array", + isRelation: true, + relationType: "shipping_methods", + }, + // Summary (specific fields) + { + value: "order.summary.total", + label: "Summary Total", + description: "Summary total amount including all adjustments", + examples: ["100.00", "250.50", "999.99"], + }, + { + value: "order.summary.subtotal", + label: "Summary Subtotal", + description: "Summary subtotal amount", + examples: ["80.00", "200.00", "850.00"], + }, + { + value: "order.summary.tax_total", + label: "Summary Tax Total", + description: "Summary tax total amount", + examples: ["20.00", "50.00", "149.99"], + }, + { + value: "order.summary.discount_total", + label: "Summary Discount Total", + description: "Summary discount total amount", + examples: ["0.00", "10.00", "50.00"], + }, + { + value: "order.summary.original_order_total", + label: "Summary Original Order Total", + description: "Summary original order total before adjustments", + examples: ["100.00", "250.50", "999.99"], + }, + { + value: "order.summary.current_order_total", + label: "Summary Current Order Total", + description: "Summary current order total after all adjustments", + examples: ["100.00", "250.50", "999.99"], + }, + { + value: "order.summary.paid_total", + label: "Summary Paid Total", + description: "Total amount that has been paid for this order", + examples: ["0.00", "100.00", "250.50"], + }, + { + value: "order.summary.refunded_total", + label: "Summary Refunded Total", + description: "Total amount that has been refunded for this order", + examples: ["0.00", "50.00", "100.00"], + }, + { + value: "order.summary.accounting_total", + label: "Summary Accounting Total", + description: "Total amount for accounting purposes", + examples: ["100.00", "250.50", "999.99"], + }, + { + value: "order.summary.credit_line_total", + label: "Summary Credit Line Total", + description: "Total amount from credit lines applied to this order", + examples: ["0.00", "25.00", "100.00"], + }, + { + value: "order.summary.transaction_total", + label: "Summary Transaction Total", + description: "Total amount of all transactions for this order", + examples: ["0.00", "100.00", "250.50"], + }, + { + value: "order.summary.pending_difference", + label: "Summary Pending Difference", + description: "Difference between expected and actual payment amounts", + examples: ["0.00", "10.00", "-5.00"], + }, + // Customer relation + { + value: "order.customer.id", + label: "Customer ID", + description: "Unique identifier of the customer who placed the order", + examples: ["cus_01ABC123"], + isRelation: true, + relationType: "customer", + }, + { + value: "order.customer.email", + label: "Customer Email", + description: "Email address of the customer who placed the order", + examples: ["customer@example.com"], + isRelation: true, + relationType: "customer", + }, + { + value: "order.customer.first_name", + label: "Customer First Name", + description: "First name of the customer", + examples: ["John", "Jane", "Jan"], + isRelation: true, + relationType: "customer", + }, + { + value: "order.customer.last_name", + label: "Customer Last Name", + description: "Last name of the customer", + examples: ["Doe", "Smith", "Kowalski"], + isRelation: true, + relationType: "customer", + }, + // Sales channel relation + { + value: "order.sales_channel.id", + label: "Sales Channel ID", + description: "Unique identifier of the sales channel", + examples: ["sc_01ABC123"], + isRelation: true, + relationType: "sales_channel", + }, + { + value: "order.sales_channel.name", + label: "Sales Channel Name", + description: "Name of the sales channel", + examples: ["Default Channel", "Online Store", "Mobile App"], + isRelation: true, + relationType: "sales_channel", + }, + // Shipping address + { + value: "order.shipping_address.first_name", + label: "Shipping First Name", + description: "First name for shipping address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["John", "Jane"], + isRelation: true, + relationType: "shipping_address", + }, + { + value: "order.shipping_address.last_name", + label: "Shipping Last Name", + description: "Last name for shipping address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Doe", "Smith"], + isRelation: true, + relationType: "shipping_address", + }, + { + value: "order.shipping_address.address_1", + label: "Shipping Address 1", + description: "Primary street address for shipping. This is an array - operator 'eq' checks if ANY value matches", + examples: ["123 Main St", "456 Oak Ave"], + isRelation: true, + relationType: "shipping_address", + }, + { + value: "order.shipping_address.city", + label: "Shipping City", + description: "City for shipping address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Warsaw", "Krakow", "London"], + isRelation: true, + relationType: "shipping_address", + }, + { + value: "order.shipping_address.country_code", + label: "Shipping Country Code", + description: "ISO 3166-1 alpha-2 country code for shipping. This is an array - operator 'eq' checks if ANY value matches", + examples: ["PL", "US", "GB", "DE"], + isRelation: true, + relationType: "shipping_address", + }, + { + value: "order.shipping_address.postal_code", + label: "Shipping Postal Code", + description: "Postal/ZIP code for shipping address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["00-001", "10001", "SW1A 1AA"], + isRelation: true, + relationType: "shipping_address", + }, + // Billing address + { + value: "order.billing_address.first_name", + label: "Billing First Name", + description: "First name for billing address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["John", "Jane"], + isRelation: true, + relationType: "billing_address", + }, + { + value: "order.billing_address.last_name", + label: "Billing Last Name", + description: "Last name for billing address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Doe", "Smith"], + isRelation: true, + relationType: "billing_address", + }, + { + value: "order.billing_address.address_1", + label: "Billing Address 1", + description: "Primary street address for billing. This is an array - operator 'eq' checks if ANY value matches", + examples: ["123 Main St", "456 Oak Ave"], + isRelation: true, + relationType: "billing_address", + }, + { + value: "order.billing_address.city", + label: "Billing City", + description: "City for billing address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Warsaw", "Krakow", "London"], + isRelation: true, + relationType: "billing_address", + }, + { + value: "order.billing_address.country_code", + label: "Billing Country Code", + description: "ISO 3166-1 alpha-2 country code for billing. This is an array - operator 'eq' checks if ANY value matches", + examples: ["PL", "US", "GB", "DE"], + isRelation: true, + relationType: "billing_address", + }, + { + value: "order.billing_address.postal_code", + label: "Billing Postal Code", + description: "Postal/ZIP code for billing address. This is an array - operator 'eq' checks if ANY value matches", + examples: ["00-001", "10001", "SW1A 1AA"], + isRelation: true, + relationType: "billing_address", + }, + // Items (specific fields, not *) + { + value: "order.items.id", + label: "Item ID", + description: "Unique identifier of the order item. This is an array - operator 'eq' checks if ANY value matches", + examples: ["item_01ABC123"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.quantity", + label: "Item Quantity", + description: "Quantity of this item in the order. This is an array - operator 'eq' checks if ANY value matches", + examples: ["1", "2", "5", "10"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.title", + label: "Item Title", + description: "Title/name of the order item. This is an array - operator 'eq' checks if ANY value matches", + examples: ["T-Shirt", "Jeans", "Sneakers"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.unit_price", + label: "Item Unit Price", + description: "Price per unit of this item. This is an array - operator 'eq' checks if ANY value matches", + examples: ["29.99", "99.99", "199.99"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.variant.id", + label: "Item Variant ID", + description: "Unique identifier of the product variant. This is an array - operator 'eq' checks if ANY value matches", + examples: ["variant_01ABC123"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.variant.sku", + label: "Item Variant SKU", + description: "SKU (Stock Keeping Unit) of the product variant. This is an array - operator 'eq' checks if ANY value matches", + examples: ["TSHIRT-SM-BLUE", "JEANS-32-BLACK"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.product.id", + label: "Item Product ID", + description: "Unique identifier of the product. This is an array - operator 'eq' checks if ANY value matches", + examples: ["prod_01ABC123"], + type: "array", + isRelation: true, + relationType: "items", + }, + { + value: "order.items.product.title", + label: "Item Product Title", + description: "Title/name of the product. This is an array - operator 'eq' checks if ANY value matches", + examples: ["T-Shirt", "Jeans", "Sneakers"], + type: "array", + isRelation: true, + relationType: "items", + }, + // Payment collections + { + value: "order.payment_collections.id", + label: "Payment Collection ID", + description: "Unique identifier of the payment collection. This is an array - operator 'eq' checks if ANY value matches", + examples: ["paycol_01ABC123"], + type: "array", + isRelation: true, + relationType: "payment_collections", + }, + { + value: "order.payment_collections.status", + label: "Payment Collection Status", + description: "Status of payment collections for this order. This is an array - operator 'eq' checks if ANY value matches", + examples: PAYMENT_COLLECTION_STATUS_VALUES, + type: "array", + isRelation: true, + relationType: "payment_collections", + }, + { + value: "order.payment_collections.amount", + label: "Payment Collection Amount", + description: "Amount of the payment collection. This is an array - operator 'eq' checks if ANY value matches", + examples: ["100.00", "250.50", "999.99"], + type: "array", + isRelation: true, + relationType: "payment_collections", + }, + // Fulfillments + { + value: "order.fulfillments.id", + label: "Fulfillment ID", + description: "Unique identifier of the fulfillment. This is an array - operator 'eq' checks if ANY value matches", + examples: ["ful_01ABC123"], + type: "array", + isRelation: true, + relationType: "fulfillments", + }, + { + value: "order.fulfillments.status", + label: "Fulfillment Status", + description: "Status of fulfillments for this order. This is an array - operator 'eq' checks if ANY value matches", + examples: FULFILLMENT_STATUS_VALUES, + type: "array", + isRelation: true, + relationType: "fulfillments", + }, +] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are required for correct totals calculation by OrderModuleService +// ORDER_QUERY_FIELDS contains all fields from ORDER_ATTRIBUTES plus technical relations +export const ORDER_QUERY_FIELDS = [ + // Basic fields from ORDER_ATTRIBUTES + ...ORDER_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for totals calculation + // These fields are not available in UI rules, but are needed for correct data retrieval + "order.items.*", + "order.items.tax_lines.*", + "order.items.adjustments.*", + "order.shipping_methods.*", + "order.shipping_methods.tax_lines.*", + "order.shipping_methods.adjustments.*", + "order.fulfillments.*", + "order.credit_lines.*", + "order.summary.*", +] diff --git a/src/modules/mpn-automation/types/modules/product-category/product-category.ts b/src/modules/mpn-automation/types/modules/product-category/product-category.ts index 977a8ec..c68c78d 100644 --- a/src/modules/mpn-automation/types/modules/product-category/product-category.ts +++ b/src/modules/mpn-automation/types/modules/product-category/product-category.ts @@ -2,41 +2,72 @@ export const PRODUCT_CATEGORY_ATTRIBUTES = [ { value: "product_category.id", label: "ID", + description: "Unique identifier of the product category", + examples: ["cat_01ABC123", "cat_01XYZ789"], }, { value: "product_category.name", label: "Name", + description: "Name of the product category", + examples: ["Clothing", "Electronics", "Home & Garden", "Sports & Outdoors"], }, { value: "product_category.description", label: "Description", + description: "Description of the product category", + examples: ["All clothing items", "Electronic devices and accessories"], }, { value: "product_category.handle", label: "Handle", + description: "URL-friendly identifier for the category (used in category URLs)", + examples: ["clothing", "electronics", "home-garden"], }, { value: "product_category.is_active", label: "Is Active", + description: "Whether the category is currently active", + examples: ["true", "false"], }, { value: "product_category.is_internal", label: "Is Internal", + description: "Whether the category is for internal use only (not visible to customers)", + examples: ["true", "false"], }, { value: "product_category.rank", label: "Rank", + description: "Display order/rank of the category", + examples: ["0", "1", "2", "10"], }, { value: "product_category.parent_category_id", label: "Parent Category ID", + description: "Unique identifier of the parent category (null for top-level categories)", + examples: ["cat_01ABC123", null], }, { value: "product_category.created_at", label: "Created At", + description: "Date and time when the category was created (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, { value: "product_category.updated_at", label: "Updated At", + description: "Date and time when the category was last updated (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, ] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are needed for correct data retrieval including all relation data +// PRODUCT_CATEGORY_QUERY_FIELDS contains all fields from PRODUCT_CATEGORY_ATTRIBUTES plus technical relations +export const PRODUCT_CATEGORY_QUERY_FIELDS = [ + // Basic fields from PRODUCT_CATEGORY_ATTRIBUTES + ...PRODUCT_CATEGORY_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for complete data retrieval (if any) + // These fields are not available in UI rules, but are needed for correct data retrieval +] diff --git a/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts b/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts index 830a46f..47f1e6e 100644 --- a/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts +++ b/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts @@ -2,17 +2,36 @@ export const PRODUCT_TAG_ATTRIBUTES = [ { value: "product_tag.id", label: "ID", + description: "Unique identifier of the product tag", + examples: ["tag_01ABC123", "tag_01XYZ789"], }, { value: "product_tag.value", label: "Value", + description: "Value/name of the product tag", + examples: ["summer", "sale", "new", "bestseller", "limited-edition"], }, { value: "product_tag.created_at", label: "Created At", + description: "Date and time when the tag was created (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, { value: "product_tag.updated_at", label: "Updated At", + description: "Date and time when the tag was last updated (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, ] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are needed for correct data retrieval including all relation data +// PRODUCT_TAG_QUERY_FIELDS contains all fields from PRODUCT_TAG_ATTRIBUTES plus technical relations +export const PRODUCT_TAG_QUERY_FIELDS = [ + // Basic fields from PRODUCT_TAG_ATTRIBUTES + ...PRODUCT_TAG_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for complete data retrieval (if any) + // These fields are not available in UI rules, but are needed for correct data retrieval +] diff --git a/src/modules/mpn-automation/types/modules/product-type/product-type.ts b/src/modules/mpn-automation/types/modules/product-type/product-type.ts index 4811aba..40092ef 100644 --- a/src/modules/mpn-automation/types/modules/product-type/product-type.ts +++ b/src/modules/mpn-automation/types/modules/product-type/product-type.ts @@ -2,17 +2,36 @@ export const PRODUCT_TYPE_ATTRIBUTES = [ { value: "product_type.id", label: "ID", + description: "Unique identifier of the product type", + examples: ["ptyp_01ABC123", "ptyp_01XYZ789"], }, { value: "product_type.value", label: "Value", + description: "Value/name of the product type", + examples: ["Clothing", "Electronics", "Accessories", "Home & Garden", "Books"], }, { value: "product_type.created_at", label: "Created At", + description: "Date and time when the product type was created (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, { value: "product_type.updated_at", label: "Updated At", + description: "Date and time when the product type was last updated (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, ] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are needed for correct data retrieval including all relation data +// PRODUCT_TYPE_QUERY_FIELDS contains all fields from PRODUCT_TYPE_ATTRIBUTES plus technical relations +export const PRODUCT_TYPE_QUERY_FIELDS = [ + // Basic fields from PRODUCT_TYPE_ATTRIBUTES + ...PRODUCT_TYPE_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for complete data retrieval (if any) + // These fields are not available in UI rules, but are needed for correct data retrieval +] diff --git a/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts b/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts index 2d7dadd..755f9c4 100644 --- a/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts +++ b/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts @@ -2,66 +2,98 @@ export const PRODUCT_VARIANT_ATTRIBUTES = [ { value: "product_variant.id", label: "ID", + description: "Unique identifier of the product variant", + examples: ["variant_01ABC123", "variant_01XYZ789"], }, { value: "product_variant.title", label: "Title", + description: "Title/name of the product variant (e.g., size and color combination)", + examples: ["Small / Blue", "Medium / Red", "Large / Black"], }, { value: "product_variant.sku", label: "SKU", + description: "SKU (Stock Keeping Unit) of the product variant", + examples: ["TSHIRT-SM-BLUE", "JEANS-32-BLACK", "SNEAKERS-42-WHITE"], }, { value: "product_variant.barcode", label: "Barcode", + description: "Barcode identifier for the product variant", + examples: ["1234567890123", "9876543210987"], }, { value: "product_variant.ean", label: "EAN", + description: "European Article Number (EAN) barcode", + examples: ["1234567890123", "9876543210987"], }, { value: "product_variant.upc", label: "UPC", + description: "Universal Product Code (UPC) barcode", + examples: ["123456789012", "987654321098"], }, { value: "product_variant.allow_backorder", label: "Allow Backorder", + description: "Whether backorders are allowed for this variant", + examples: ["true", "false"], }, { value: "product_variant.manage_inventory", label: "Manage Inventory", + description: "Whether inventory is managed for this variant", + examples: ["true", "false"], }, { value: "product_variant.hs_code", label: "HS Code", + description: "Harmonized System (HS) code for customs classification", + examples: ["6109.10.00", "6403.99.00"], }, { value: "product_variant.origin_country", label: "Origin Country", + description: "ISO 3166-1 alpha-2 country code where the variant originates", + examples: ["PL", "US", "CN", "DE"], }, { value: "product_variant.mid_code", label: "MID Code", + description: "Manufacturer Identification (MID) code", + examples: ["MID123456"], }, { value: "product_variant.material", label: "Material", + description: "Material composition of the product variant", + examples: ["Cotton", "Polyester", "Leather", "Metal"], }, { value: "product_variant.weight", label: "Weight", + description: "Weight of the product variant in grams", + examples: ["100", "500", "1000", "2500"], }, { value: "product_variant.length", label: "Length", + description: "Length of the product variant in centimeters", + examples: ["10", "20", "30", "50"], }, { value: "product_variant.height", label: "Height", + description: "Height of the product variant in centimeters", + examples: ["5", "10", "15", "25"], }, { value: "product_variant.width", label: "Width", + description: "Width of the product variant in centimeters", + examples: ["10", "20", "30", "40"], }, // { // value: "product_variant.metadata", @@ -70,17 +102,36 @@ export const PRODUCT_VARIANT_ATTRIBUTES = [ { value: "product_variant.variant_rank", label: "Variant Rank", + description: "Display order/rank of the variant", + examples: ["0", "1", "2", "10"], }, { value: "product_variant.product_id", label: "Product ID", + description: "Unique identifier of the parent product", + examples: ["prod_01ABC123"], }, { value: "product_variant.created_at", label: "Created At", + description: "Date and time when the variant was created (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, { value: "product_variant.updated_at", label: "Updated At", + description: "Date and time when the variant was last updated (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, ] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are needed for correct data retrieval including all relation data +// PRODUCT_VARIANT_QUERY_FIELDS contains all fields from PRODUCT_VARIANT_ATTRIBUTES plus technical relations +export const PRODUCT_VARIANT_QUERY_FIELDS = [ + // Basic fields from PRODUCT_VARIANT_ATTRIBUTES + ...PRODUCT_VARIANT_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for complete data retrieval (if any) + // These fields are not available in UI rules, but are needed for correct data retrieval +] diff --git a/src/modules/mpn-automation/types/modules/product/helpers.ts b/src/modules/mpn-automation/types/modules/product/helpers.ts new file mode 100644 index 0000000..b6bf320 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product/helpers.ts @@ -0,0 +1,7 @@ +import { ProductStatus } from "@medusajs/framework/utils" + +/** + * Helper to get all possible ProductStatus values + */ +export const PRODUCT_STATUS_VALUES = Object.values(ProductStatus) as string[] + diff --git a/src/modules/mpn-automation/types/modules/product/product.ts b/src/modules/mpn-automation/types/modules/product/product.ts index 38d8209..3a9d300 100644 --- a/src/modules/mpn-automation/types/modules/product/product.ts +++ b/src/modules/mpn-automation/types/modules/product/product.ts @@ -1,99 +1,149 @@ +import { PRODUCT_STATUS_VALUES } from "./helpers" + export const PRODUCT_ATTRIBUTES = [ { value: "product.id", label: "ID", + description: "Unique identifier of the product", + examples: ["prod_01ABC123", "prod_01XYZ789"], }, { value: "product.title", label: "Title", + description: "Title/name of the product", + examples: ["T-Shirt", "Jeans", "Sneakers", "Laptop"], }, { value: "product.handle", label: "Handle", + description: "URL-friendly identifier for the product (used in product URLs)", + examples: ["t-shirt", "blue-jeans", "running-sneakers"], }, { value: "product.subtitle", label: "Subtitle", + description: "Subtitle or short description of the product", + examples: ["Comfortable cotton t-shirt", "Premium denim jeans"], }, { value: "product.description", label: "Description", + description: "Full description of the product", + examples: ["This is a high-quality product...", "Made from premium materials..."], }, { value: "product.is_giftcard", label: "Is Giftcard", + description: "Whether this product is a gift card", + examples: ["true", "false"], }, { value: "product.status", label: "Status", + description: "Current status of the product", + examples: PRODUCT_STATUS_VALUES, }, { value: "product.sku", label: "SKU", + description: "SKU (Stock Keeping Unit) of the product", + examples: ["TSHIRT-001", "JEANS-BLUE-32"], }, { value: "product.barcode", label: "Barcode", + description: "Barcode identifier for the product", + examples: ["1234567890123", "9876543210987"], }, { value: "product.ean", label: "EAN", + description: "European Article Number (EAN) barcode", + examples: ["1234567890123", "9876543210987"], }, { value: "product.upc", label: "UPC", + description: "Universal Product Code (UPC) barcode", + examples: ["123456789012", "987654321098"], }, { value: "product.thumbnail", label: "Thumbnail", + description: "URL of the product thumbnail image", + examples: ["https://example.com/image.jpg"], }, { value: "product.hs_code", label: "HS Code", + description: "Harmonized System (HS) code for customs classification", + examples: ["6109.10.00", "6403.99.00"], }, { value: "product.origin_country", label: "Origin Country", + description: "ISO 3166-1 alpha-2 country code where the product originates", + examples: ["PL", "US", "CN", "DE"], }, { value: "product.mid_code", label: "MID Code", + description: "Manufacturer Identification (MID) code", + examples: ["MID123456"], }, { value: "product.material", label: "Material", + description: "Material composition of the product", + examples: ["Cotton", "Polyester", "Leather", "Metal"], }, { value: "product.weight", label: "Weight", + description: "Weight of the product in grams", + examples: ["100", "500", "1000", "2500"], }, { value: "product.length", label: "Length", + description: "Length of the product in centimeters", + examples: ["10", "20", "30", "50"], }, { value: "product.height", label: "Height", + description: "Height of the product in centimeters", + examples: ["5", "10", "15", "25"], }, { value: "product.width", label: "Width", + description: "Width of the product in centimeters", + examples: ["10", "20", "30", "40"], }, { value: "product.created_at", label: "Created At", + description: "Date and time when the product was created (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, { value: "product.updated_at", label: "Updated At", + description: "Date and time when the product was last updated (ISO 8601 format)", + examples: ["2024-01-15T10:30:00Z", "2024-12-25T00:00:00Z"], }, { value: "product.deleted_at", label: "Deleted At", + description: "Date and time when the product was deleted (ISO 8601 format), null if not deleted", + examples: ["2024-01-15T10:30:00Z", null], }, { value: "product.tags.id", label: "Tag ID", + description: "Unique identifier of the product tag. This is an array - operator 'eq' checks if ANY value matches", + examples: ["tag_01ABC123"], type: "array", isRelation: true, relationType: "tags", @@ -101,6 +151,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.tags.value", label: "Tag Value", + description: "Value/name of the product tag. This is an array - operator 'eq' checks if ANY value matches", + examples: ["summer", "sale", "new", "bestseller"], type: "array", isRelation: true, relationType: "tags", @@ -108,6 +160,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.categories.id", label: "Category ID", + description: "Unique identifier of the product category. This is an array - operator 'eq' checks if ANY value matches", + examples: ["cat_01ABC123"], type: "array", isRelation: true, relationType: "categories", @@ -115,6 +169,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.categories.name", label: "Category Name", + description: "Name of the product category. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Clothing", "Electronics", "Home & Garden"], type: "array", isRelation: true, relationType: "categories", @@ -122,6 +178,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.categories.handle", label: "Category Handle", + description: "URL-friendly identifier of the category. This is an array - operator 'eq' checks if ANY value matches", + examples: ["clothing", "electronics", "home-garden"], type: "array", isRelation: true, relationType: "categories", @@ -129,6 +187,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.variants.id", label: "Variant ID", + description: "Unique identifier of the product variant. This is an array - operator 'eq' checks if ANY value matches", + examples: ["variant_01ABC123"], type: "array", isRelation: true, relationType: "variants", @@ -136,6 +196,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.variants.sku", label: "Variant SKU", + description: "SKU of the product variant. This is an array - operator 'eq' checks if ANY value matches", + examples: ["TSHIRT-SM-BLUE", "JEANS-32-BLACK"], type: "array", isRelation: true, relationType: "variants", @@ -143,6 +205,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.variants.title", label: "Variant Title", + description: "Title/name of the product variant. This is an array - operator 'eq' checks if ANY value matches", + examples: ["Small / Blue", "32 / Black", "Large / Red"], type: "array", isRelation: true, relationType: "variants", @@ -150,6 +214,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.type.id", label: "Type ID", + description: "Unique identifier of the product type", + examples: ["ptyp_01ABC123"], type: "object", isRelation: true, relationType: "type", @@ -157,6 +223,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.type.value", label: "Type Value", + description: "Value/name of the product type", + examples: ["Clothing", "Electronics", "Accessories"], type: "object", isRelation: true, relationType: "type", @@ -164,6 +232,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.collection.id", label: "Collection ID", + description: "Unique identifier of the product collection", + examples: ["pcol_01ABC123"], type: "object", isRelation: true, relationType: "collection", @@ -171,6 +241,8 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.collection.title", label: "Collection Title", + description: "Title/name of the product collection", + examples: ["Summer Collection", "Winter Sale", "New Arrivals"], type: "object", isRelation: true, relationType: "collection", @@ -178,8 +250,26 @@ export const PRODUCT_ATTRIBUTES = [ { value: "product.collection.handle", label: "Collection Handle", + description: "URL-friendly identifier of the collection", + examples: ["summer-collection", "winter-sale", "new-arrivals"], type: "object", isRelation: true, relationType: "collection", }, ] + +// Fields for use in query.graph() - includes technical relations with * +// These fields are needed for correct data retrieval including all relation data +// PRODUCT_QUERY_FIELDS contains all fields from PRODUCT_ATTRIBUTES plus technical relations +export const PRODUCT_QUERY_FIELDS = [ + // Basic fields from PRODUCT_ATTRIBUTES + ...PRODUCT_ATTRIBUTES.map((attr) => attr.value), + + // Technical relations required for complete data retrieval + // These fields are not available in UI rules, but are needed for correct data retrieval + "product.tags.*", + "product.categories.*", + "product.variants.*", + "product.type.*", + "product.collection.*", +] diff --git a/src/modules/mpn-automation/types/types.ts b/src/modules/mpn-automation/types/types.ts index 8640d1f..53912f1 100644 --- a/src/modules/mpn-automation/types/types.ts +++ b/src/modules/mpn-automation/types/types.ts @@ -43,6 +43,16 @@ export type CustomAction = { export type Attribute = { value?: string label?: string + /** + * Description of what this attribute represents + * Example: "Total amount of the order including taxes and shipping" + */ + description?: string + /** + * Example values for this attribute + * Example: ["completed", "pending", "canceled"] for status fields + */ + examples?: string[] /** * Type of the attribute value * - "primitive": single value (string, number, boolean) diff --git a/src/subscribers/inventory-level-updated.ts b/src/subscribers/inventory-level-updated.ts index 3070375..8f0935b 100644 --- a/src/subscribers/inventory-level-updated.ts +++ b/src/subscribers/inventory-level-updated.ts @@ -46,7 +46,7 @@ export default async function inventoryLevelUpdatedHandler({ input: { eventName: eventName, eventType: TriggerType.EVENT, - triggerKey: `inventory_level-${id}`, + triggerKey: `inventory_level-updated-${id}`, context: contextData, contextType: "inventory-level", }, diff --git a/src/subscribers/order-updated.ts b/src/subscribers/order-updated.ts new file mode 100644 index 0000000..f7ac0be --- /dev/null +++ b/src/subscribers/order-updated.ts @@ -0,0 +1,49 @@ +import { + SubscriberArgs, + type SubscriberConfig, +} from "@medusajs/medusa" +import { getOrderByIdWorkflow } from "../workflows/order/get-order-by-id" +import { runAutomationWorkflow } from "../workflows/mpn-automation/run-automation" +import { TriggerType } from "../utils/types" + +const eventName = "order.updated" + +export default async function orderUpdatedHandler({ + event: { + data: { id }, + }, + container, +}: SubscriberArgs) { + // Retrieve inventory level with related inventory item + const { + result: { order }, + } = await getOrderByIdWorkflow(container).run({ + input: { + order_id: id, + }, + }) + + const contextData = { + order: order, + } + + // Run automation workflow - this will: + // 1. Retrieve triggers for the event + // 2. Validate triggers against context + // 3. Execute actions for validated triggers + const { result } = await runAutomationWorkflow( + container + ).run({ + input: { + eventName: eventName, + eventType: TriggerType.EVENT, + triggerKey: `order-${id}`, + context: contextData, + contextType: "order", + }, + }) +} + +export const config: SubscriberConfig = { + event: eventName, +} diff --git a/src/templates/slack/order/base-order.ts b/src/templates/slack/order/base-order.ts new file mode 100644 index 0000000..5d4f9ad --- /dev/null +++ b/src/templates/slack/order/base-order.ts @@ -0,0 +1,87 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { translations } from "./translations" +import { + createTranslator, + mergeTranslations, +} from "../../../utils" + +export type OrderEventType = "placed" | "completed" | "updated" | "canceled" | "archived" + +export interface RenderOrderBaseParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions + eventType: OrderEventType +} + +/** + * Base function for rendering order Slack templates + * Handles common logic for all order event types + */ +export function renderOrderBase({ + context, + contextType, + options = {}, + eventType, +}: RenderOrderBaseParams): { + text: string + blocks: SlackBlock[] +} { + const backendUrl = options?.backendUrl || "" + const locale = options?.locale || "pl" + const order = context?.order + + // Merge custom translations if provided + const mergedTranslations = mergeTranslations( + translations, + options.customTranslations + ) + + // Create translator function + const t = createTranslator(locale, mergedTranslations) + + const blocks: SlackBlock[] = [] + + if (order?.id) { + blocks.push({ + type: "header", + text: { + type: "plain_text", + text: t(`${eventType}.header.title`, { + orderId: order?.display_id || order?.id || "unknown", + }), + emoji: true, + }, + }) + } + + if (order?.id) { + // Use "danger" style for canceled orders, "primary" for others + const buttonStyle = eventType === "canceled" ? "danger" : "primary" + + blocks.push({ + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: t("actions.openInPanel"), + }, + url: `${backendUrl}/app/orders/${order.id}`, + style: buttonStyle, + }, + ], + }) + } + + blocks.push({ type: "divider" }) + + return { + text: t(`${eventType}.headerTitle`, { + orderId: order?.display_id || order?.id || "unknown", + }), + blocks, + } +} + diff --git a/src/templates/slack/order/index.ts b/src/templates/slack/order/index.ts new file mode 100644 index 0000000..163e306 --- /dev/null +++ b/src/templates/slack/order/index.ts @@ -0,0 +1,6 @@ +export * from "./order" +export * from "./order-placed" +export * from "./order-completed" +export * from "./order-updated" +export * from "./order-canceled" +export * from "./order-archived" diff --git a/src/templates/slack/order/order-archived.ts b/src/templates/slack/order/order-archived.ts new file mode 100644 index 0000000..970c577 --- /dev/null +++ b/src/templates/slack/order/order-archived.ts @@ -0,0 +1,25 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { renderOrderBase } from "./base-order" + +export interface RenderOrderArchivedParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions +} + +export function renderOrderArchived({ + context, + contextType, + options = {}, +}: RenderOrderArchivedParams): { + text: string + blocks: SlackBlock[] +} { + return renderOrderBase({ + context, + contextType, + options, + eventType: "archived", + }) +} + diff --git a/src/templates/slack/order/order-canceled.ts b/src/templates/slack/order/order-canceled.ts new file mode 100644 index 0000000..9a1cc80 --- /dev/null +++ b/src/templates/slack/order/order-canceled.ts @@ -0,0 +1,25 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { renderOrderBase } from "./base-order" + +export interface RenderOrderCanceledParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions +} + +export function renderOrderCanceled({ + context, + contextType, + options = {}, +}: RenderOrderCanceledParams): { + text: string + blocks: SlackBlock[] +} { + return renderOrderBase({ + context, + contextType, + options, + eventType: "canceled", + }) +} + diff --git a/src/templates/slack/order/order-completed.ts b/src/templates/slack/order/order-completed.ts new file mode 100644 index 0000000..d7b06d9 --- /dev/null +++ b/src/templates/slack/order/order-completed.ts @@ -0,0 +1,25 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { renderOrderBase } from "./base-order" + +export interface RenderOrderCompletedParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions +} + +export function renderOrderCompleted({ + context, + contextType, + options = {}, +}: RenderOrderCompletedParams): { + text: string + blocks: SlackBlock[] +} { + return renderOrderBase({ + context, + contextType, + options, + eventType: "completed", + }) +} + diff --git a/src/templates/slack/order/order-placed.ts b/src/templates/slack/order/order-placed.ts new file mode 100644 index 0000000..eb8b4d3 --- /dev/null +++ b/src/templates/slack/order/order-placed.ts @@ -0,0 +1,25 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { renderOrderBase } from "./base-order" + +export interface RenderOrderPlacedParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions +} + +export function renderOrderPlaced({ + context, + contextType, + options = {}, +}: RenderOrderPlacedParams): { + text: string + blocks: SlackBlock[] +} { + return renderOrderBase({ + context, + contextType, + options, + eventType: "placed", + }) +} + diff --git a/src/templates/slack/order/order-updated.ts b/src/templates/slack/order/order-updated.ts new file mode 100644 index 0000000..02e7a4f --- /dev/null +++ b/src/templates/slack/order/order-updated.ts @@ -0,0 +1,25 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { renderOrderBase } from './base-order' + +export interface RenderOrderUpdatedParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions +} + +export function renderOrderUpdated({ + context, + contextType, + options = {}, +}: RenderOrderUpdatedParams): { + text: string + blocks: SlackBlock[] +} { + return renderOrderBase({ + context, + contextType, + options, + eventType: "updated", + }) +} + diff --git a/src/templates/slack/order/order.ts b/src/templates/slack/order/order.ts new file mode 100644 index 0000000..c9b9235 --- /dev/null +++ b/src/templates/slack/order/order.ts @@ -0,0 +1,78 @@ +import { SlackTemplateOptions, SlackBlock } from "../types" +import { translations } from "./translations" +import { + createTranslator, + mergeTranslations, +} from "../../../utils" + +export interface RenderOrderParams { + context: any + contextType?: string | null + options?: SlackTemplateOptions +} + +export function renderOrder({ + context, + contextType, + options = {}, +}: RenderOrderParams): { + text: string + blocks: SlackBlock[] +} { + const backendUrl = options?.backendUrl || "" + const locale = options?.locale || "pl" + const order = context?.order + + // Merge custom translations if provided + const mergedTranslations = mergeTranslations( + translations, + options.customTranslations + ) + + // Create translator function + const t = createTranslator(locale, mergedTranslations) + + const blocksSections: SlackBlock[] = [] + + if (order?.id) { + blocksSections.push({ + type: "header", + text: { + type: "plain_text", + text: t("header.title", { + orderId: order?.id || "unknown", + }), + emoji: true, + }, + }) + } + + const blocks: SlackBlock[] = + blocksSections.length > 0 ? blocksSections : [] + + if (order?.id) { + blocks.push({ + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: t("actions.openInPanel"), + }, + url: `${backendUrl}/app/orders/${order.id}`, + style: "primary", + }, + ], + }) + } + + blocks.push({ type: "divider" }) + + return { + text: t("headerTitle", { + orderId: order?.id || "unknown", + }), + blocks, + } +} diff --git a/src/templates/slack/order/translations/en.json b/src/templates/slack/order/translations/en.json new file mode 100644 index 0000000..594ac47 --- /dev/null +++ b/src/templates/slack/order/translations/en.json @@ -0,0 +1,39 @@ +{ + "placed": { + "header": { + "title": "🎉 New order #{{orderId}}" + }, + "headerTitle": "New order #{{orderId}}" + }, + "completed": { + "header": { + "title": "✅ Order #{{orderId}} has been completed" + }, + "headerTitle": "Order #{{orderId}} has been completed" + }, + "updated": { + "header": { + "title": "📝 Order #{{orderId}} has been updated" + }, + "headerTitle": "Order #{{orderId}} has been updated" + }, + "canceled": { + "header": { + "title": "❌ Order #{{orderId}} has been canceled" + }, + "headerTitle": "Order #{{orderId}} has been canceled" + }, + "archived": { + "header": { + "title": "📦 Order #{{orderId}} has been archived" + }, + "headerTitle": "Order #{{orderId}} has been archived" + }, + "actions": { + "openInPanel": "Open in panel" + }, + "labels": { + "noData": "No data" + } +} + diff --git a/src/templates/slack/order/translations/index.ts b/src/templates/slack/order/translations/index.ts new file mode 100644 index 0000000..6bbe4d8 --- /dev/null +++ b/src/templates/slack/order/translations/index.ts @@ -0,0 +1,9 @@ +import pl from "./pl.json" +import en from "./en.json" + +export const translations: Record = { + pl: pl, + en: en, +} + +export { pl, en } diff --git a/src/templates/slack/order/translations/pl.json b/src/templates/slack/order/translations/pl.json new file mode 100644 index 0000000..1d043cf --- /dev/null +++ b/src/templates/slack/order/translations/pl.json @@ -0,0 +1,39 @@ +{ + "placed": { + "header": { + "title": "🎉 Nowe zamówienie #{{orderId}}" + }, + "headerTitle": "Nowe zamówienie #{{orderId}}" + }, + "completed": { + "header": { + "title": "✅ Zamówienie #{{orderId}} zostało zrealizowane" + }, + "headerTitle": "Zamówienie #{{orderId}} zostało zrealizowane" + }, + "updated": { + "header": { + "title": "📝 Zamówienie #{{orderId}} zostało zaktualizowane" + }, + "headerTitle": "Zamówienie #{{orderId}} zostało zaktualizowane" + }, + "canceled": { + "header": { + "title": "❌ Zamówienie #{{orderId}} zostało anulowane" + }, + "headerTitle": "Zamówienie #{{orderId}} zostało anulowane" + }, + "archived": { + "header": { + "title": "📦 Zamówienie #{{orderId}} zostało zarchiwizowane" + }, + "headerTitle": "Zamówienie #{{orderId}} zostało zarchiwizowane" + }, + "actions": { + "openInPanel": "Otwórz w panelu" + }, + "labels": { + "noData": "Brak danych" + } +} + diff --git a/src/workflows/index.ts b/src/workflows/index.ts index 7657bb8..cbef375 100644 --- a/src/workflows/index.ts +++ b/src/workflows/index.ts @@ -1,5 +1,6 @@ export * from "./inventory" export * from "./mpn-automation" export * from "./notifications" +export * from "./order" export * from "./product" export * from "./product-variant" diff --git a/src/workflows/inventory/steps/get-inventory-level-by-id.ts b/src/workflows/inventory/steps/get-inventory-level-by-id.ts index 588ef21..68d62fb 100644 --- a/src/workflows/inventory/steps/get-inventory-level-by-id.ts +++ b/src/workflows/inventory/steps/get-inventory-level-by-id.ts @@ -10,7 +10,7 @@ import { StepResponse, createStep, } from "@medusajs/framework/workflows-sdk" -import { INVENTORY_LEVEL_ATTRIBUTES } from "../../../modules/mpn-automation/types/modules/inventory" +import { INVENTORY_LEVEL_QUERY_FIELDS } from "../../../modules/mpn-automation/types/modules/inventory" import { getFieldsFromAttributes } from "../../../utils" export interface GetInventoryLevelByIdStepInput { @@ -54,11 +54,9 @@ export const getInventoryLevelByIdStep = createStep( ) } - // Generate fields from INVENTORY_LEVEL_ATTRIBUTES to keep them in sync + // Generate fields from INVENTORY_LEVEL_QUERY_FIELDS which includes technical relations needed for complete data retrieval const fields = getFieldsFromAttributes( - INVENTORY_LEVEL_ATTRIBUTES as Array<{ - value?: string - }>, + INVENTORY_LEVEL_QUERY_FIELDS.map((field) => ({ value: field })), "inventory_level" ) diff --git a/src/workflows/mpn-automation/index.ts b/src/workflows/mpn-automation/index.ts index 383fbd1..923d37b 100644 --- a/src/workflows/mpn-automation/index.ts +++ b/src/workflows/mpn-automation/index.ts @@ -1,4 +1,4 @@ -export * from "./validate-automation-triggers-by-event" +export * from "./validate-triggers-by-event" export * from "./run-automation" export * from "./run-email-action" export * from "./run-slack-action" diff --git a/src/workflows/mpn-automation/run-automation.ts b/src/workflows/mpn-automation/run-automation.ts index af53f81..cdb37d6 100644 --- a/src/workflows/mpn-automation/run-automation.ts +++ b/src/workflows/mpn-automation/run-automation.ts @@ -4,35 +4,68 @@ import { WorkflowResponse, transform, } from "@medusajs/framework/workflows-sdk" -import { validateAutomationTriggersByEventWorkflow } from "./validate-automation-triggers-by-event" +import { validateTriggersByEventWorkflow } from "./validate-triggers-by-event" +import { validateTriggerThrottleStep } from "./steps/validate-trigger-throttle" import { runAutomationActionsStep } from "./steps/run-automation-actions" import { saveAutomationStateWorkflow } from "./save-automation-state" import { TriggerType } from "../../utils/types" import { logStep } from "../../workflows/steps/log-step" export interface RunAutomationWorkflowInput { + /** + * Event name to match triggers (e.g. "order.placed") + */ eventName: string + /** + * Type of trigger: "event", "schedule", or "manual" + */ eventType: TriggerType + /** + * Unique key for throttle tracking (e.g. order_id) + */ triggerKey: string + /** + * Event payload data for rules evaluation and actions + */ context: Record + /** + * Optional context type identifier + */ contextType?: string | null } export interface RunAutomationWorkflowOutput { - triggersFound: number - triggersValidated: number - triggersExecuted: number - totalActionsExecuted: number - results: Array<{ - triggerId?: string - isValid: boolean - actionsExecuted: number - actions: Array<{ - actionId?: string - actionType?: string | null - success: boolean - }> - }> + /** + * All active triggers found for this event + */ + triggers: any[] + /** + * Triggers that passed rules validation + */ + triggersValidated: any[] + /** + * Triggers blocked by throttle (interval_seconds not passed) + */ + triggersThrottled: any[] + /** + * Triggers that passed rules AND throttle check + */ + triggersPassedThrottle: any[] + /** + * Triggers with actions executed + */ + triggersExecuted: any[] + /** + * States saved for throttle tracking + */ + statesSaved: any[] + + triggersCount: number + triggersValidatedCount: number + triggersThrottledCount: number + triggersPassedThrottleCount: number + triggersExecutedCount: number + statesSavedCount: number } export const runAutomationWorkflowId = "run-automation" @@ -42,8 +75,10 @@ export const runAutomationWorkflowId = "run-automation" * * This workflow: * 1. Retrieves all active triggers for the event - * 2. Validates triggers against the provided context - * 3. Executes actions for triggers that passed validation + * 2. Validates triggers against rules (conditions) + * 3. Checks throttle limits (interval_seconds) + * 4. Executes actions for triggers that passed validation and throttle + * 5. Saves automation trigger state for throttle tracking * * @example * ```typescript @@ -67,9 +102,11 @@ export const runAutomationWorkflowId = "run-automation" export const runAutomationWorkflow = createWorkflow( runAutomationWorkflowId, (input: WorkflowData) => { - // Step 1: Retrieve and validate triggers by the event + /** + * Step 1: Retrieve and validate triggers by the event (rules validation) + */ const getValidationResult = - validateAutomationTriggersByEventWorkflow.runAsStep({ + validateTriggersByEventWorkflow.runAsStep({ input: { eventName: input.eventName, eventType: input.eventType, @@ -77,17 +114,47 @@ export const runAutomationWorkflow = createWorkflow( }, }) - // Step 2: Run actions for all validated triggers + /** + * Step 2: Check throttle limits for validated triggers + */ + const getTriggerThrottleResult = validateTriggerThrottleStep({ + validatedTriggers: + getValidationResult.triggersValidated, + targetKey: input.triggerKey, + }) + + /** + * Step 3: Transform throttle results to format expected by runAutomationActionsStep + */ + const triggersAfterThrottle = transform( + { getTriggerThrottleResult }, + (data) => { + const results = data.getTriggerThrottleResult || [] + // Filter to only non-throttled, valid triggers + return results + .filter((r: any) => r.isValid && !r.isThrottled) + .map((r: any) => ({ + isValid: r.isValid, + trigger: r.trigger, + actions: r.trigger.actions || [], + })) + } + ) + + /** + * Step 4: Run actions for triggers that passed throttle check + */ const getActionRunningResult = runAutomationActionsStep( { - validatedTriggers: - getValidationResult.triggersValidated, + validatedTriggers: triggersAfterThrottle, context: input.context, contextType: input.contextType, } ) - // Step 3: Save automation state + /** + * Step 5: Save automation state + */ const getSaveAutomationStateResult = saveAutomationStateWorkflow.runAsStep({ input: { @@ -96,10 +163,13 @@ export const runAutomationWorkflow = createWorkflow( }, }) - // Combine all results + /** + * Combine all results + */ const finalResult = transform( { getValidationResult, + getTriggerThrottleResult, getActionRunningResult, getSaveAutomationStateResult, }, @@ -108,6 +178,14 @@ export const runAutomationWorkflow = createWorkflow( data.getValidationResult.triggers || [] const triggersValidated = data.getValidationResult.triggersValidated || [] + const throttleResults = data.getTriggerThrottleResult || [] + const triggersThrottled = throttleResults.filter( + (r: any) => r.isThrottled + ) + const triggersPassedThrottle = + throttleResults.filter( + (r: any) => !r.isThrottled && r.isValid + ) const triggersExecuted = data.getActionRunningResult.triggersExecuted || [] const statesSaved = @@ -117,11 +195,17 @@ export const runAutomationWorkflow = createWorkflow( return { triggers, triggersValidated, + triggersThrottled, + triggersPassedThrottle, triggersExecuted, statesSaved, triggersCount: triggers.length || 0, triggersValidatedCount: triggersValidated.length || 0, + triggersThrottledCount: + triggersThrottled.length || 0, + triggersPassedThrottleCount: + triggersPassedThrottle.length || 0, triggersExecutedCount: triggersExecuted.length || 0, statesSavedCount: statesSaved.length || 0, @@ -129,6 +213,9 @@ export const runAutomationWorkflow = createWorkflow( } ) + /** + * Log the final result + */ logStep(finalResult) return new WorkflowResponse(finalResult) diff --git a/src/workflows/mpn-automation/steps/create-automation.ts b/src/workflows/mpn-automation/steps/create-automation.ts index 96ad1d2..4e2c990 100644 --- a/src/workflows/mpn-automation/steps/create-automation.ts +++ b/src/workflows/mpn-automation/steps/create-automation.ts @@ -26,7 +26,7 @@ export const createAutomationStep = createStep( description: item.description, trigger_type: item.trigger_type, event_name: item.event_name, - interval_minutes: item.interval_minutes, + interval_seconds: item.interval_seconds, active: item.active, channels: item.channels, })) diff --git a/src/workflows/mpn-automation/steps/edit-automation.ts b/src/workflows/mpn-automation/steps/edit-automation.ts index 969d10b..580755e 100644 --- a/src/workflows/mpn-automation/steps/edit-automation.ts +++ b/src/workflows/mpn-automation/steps/edit-automation.ts @@ -27,7 +27,7 @@ export const editAutomationStep = createStep( description: item.description, trigger_type: item.trigger_type, event_name: item.event_name, - interval_minutes: item.interval_minutes, + interval_seconds: item.interval_seconds, active: item.active, channels: item.channels, })) diff --git a/src/workflows/mpn-automation/steps/index.ts b/src/workflows/mpn-automation/steps/index.ts index f03d8bc..c39e5f4 100644 --- a/src/workflows/mpn-automation/steps/index.ts +++ b/src/workflows/mpn-automation/steps/index.ts @@ -1,5 +1,6 @@ export * from "./retrieve-automation-triggers-by-event" -export * from "./validate-automation-triggers" +export * from "./validate-triggers-rules" +export * from "./validate-trigger-throttle" export * from "./run-automation-actions" export * from "./create-automation" export * from "./edit-automation" diff --git a/src/workflows/mpn-automation/steps/retrieve-automation-triggers-by-event.ts b/src/workflows/mpn-automation/steps/retrieve-automation-triggers-by-event.ts index 48df6b7..957ab36 100644 --- a/src/workflows/mpn-automation/steps/retrieve-automation-triggers-by-event.ts +++ b/src/workflows/mpn-automation/steps/retrieve-automation-triggers-by-event.ts @@ -60,7 +60,7 @@ export const getAutomationTriggersByEventStep = createStep( description: trigger.description, trigger_type: trigger.trigger_type, event_name: trigger.event_name, - interval_minutes: trigger.interval_minutes, + interval_seconds: trigger.interval_seconds, active: trigger.active, channels: trigger.channels, metadata: trigger.metadata, diff --git a/src/workflows/mpn-automation/steps/run-automation-actions.ts b/src/workflows/mpn-automation/steps/run-automation-actions.ts index eb4ff1f..c8f7319 100644 --- a/src/workflows/mpn-automation/steps/run-automation-actions.ts +++ b/src/workflows/mpn-automation/steps/run-automation-actions.ts @@ -9,7 +9,6 @@ import { AutomationTrigger, } from "../../../modules/mpn-automation/types/interfaces" import MpnAutomationService from "../../../modules/mpn-automation/services/service" -import { saveAutomationStateWorkflow } from "../save-automation-state" export interface RunAutomationActionsStepInput { validatedTriggers: Array<{ diff --git a/src/workflows/mpn-automation/steps/validate-trigger-throttle.ts b/src/workflows/mpn-automation/steps/validate-trigger-throttle.ts new file mode 100644 index 0000000..496c984 --- /dev/null +++ b/src/workflows/mpn-automation/steps/validate-trigger-throttle.ts @@ -0,0 +1,128 @@ +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import MpnAutomationService from "../../../modules/mpn-automation/services/service" +import { MPN_AUTOMATION_MODULE } from "../../../modules/mpn-automation" +import { AutomationTrigger } from "../../../modules/mpn-automation/types/interfaces" + +export interface ValidateTriggerThrottleStepInput { + validatedTriggers: Array<{ + isValid: boolean + trigger: AutomationTrigger + }> + targetKey: string | null +} + +export interface ValidateTriggerThrottleResult { + trigger: AutomationTrigger + isValid: boolean + isThrottled: boolean + throttleReason?: string + nextAvailableAt?: Date +} + +export const validateTriggerThrottleStepId = "validate-trigger-throttle" + +/** + * This step checks throttle limits for validated triggers. + * Filters out triggers that are throttled based on interval_seconds and MpnAutomationState. + * + * For event triggers with interval_seconds set: + * - Checks last_triggered_at from MpnAutomationState + * - Skips trigger if not enough time has passed since last execution + * + * @example + * const data = validateTriggerThrottleStep({ + * validatedTriggers: [...], + * targetKey: "order_123" + * }) + */ +export const validateTriggerThrottleStep = createStep( + validateTriggerThrottleStepId, + async ( + input: ValidateTriggerThrottleStepInput, + { container } + ): Promise> => { + const { validatedTriggers, targetKey } = input + + const mpnAutomationService: MpnAutomationService = + container.resolve(MPN_AUTOMATION_MODULE) + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + + if ( + !validatedTriggers || + validatedTriggers.length === 0 + ) { + return new StepResponse([], []) + } + + const results: ValidateTriggerThrottleResult[] = [] + + for (const validated of validatedTriggers) { + const trigger = validated.trigger + + // Only check throttle for event triggers with interval_seconds set + if ( + trigger.trigger_type !== "event" || + !trigger.interval_seconds || + trigger.interval_seconds <= 0 + ) { + // No throttle configured - pass through + results.push({ + trigger, + isValid: validated.isValid, + isThrottled: false, + }) + continue + } + + // Check state for this trigger + target + const states = + await mpnAutomationService.listMpnAutomationStates({ + trigger_id: trigger.id, + target_key: targetKey, + }) + + const state = states?.[0] + + if (state?.last_triggered_at) { + const lastTriggeredAt = new Date( + state.last_triggered_at + ) + const now = new Date() + const secondsSinceLast = + (now.getTime() - lastTriggeredAt.getTime()) / 1000 + + if (secondsSinceLast < trigger.interval_seconds) { + // Throttled - not enough time has passed + const nextAvailableAt = new Date( + lastTriggeredAt.getTime() + + trigger.interval_seconds * 1000 + ) + + results.push({ + trigger, + isValid: false, + isThrottled: true, + throttleReason: `Throttled: last triggered ${Math.round(secondsSinceLast)}s ago, minimum interval is ${trigger.interval_seconds}s`, + nextAvailableAt, + }) + continue + } + } + + // Not throttled - pass through + results.push({ + trigger, + isValid: validated.isValid, + isThrottled: false, + }) + } + + return new StepResponse(results, results) + } +) diff --git a/src/workflows/mpn-automation/steps/validate-automation-triggers.ts b/src/workflows/mpn-automation/steps/validate-triggers-rules.ts similarity index 74% rename from src/workflows/mpn-automation/steps/validate-automation-triggers.ts rename to src/workflows/mpn-automation/steps/validate-triggers-rules.ts index e581032..af8a89c 100644 --- a/src/workflows/mpn-automation/steps/validate-automation-triggers.ts +++ b/src/workflows/mpn-automation/steps/validate-triggers-rules.ts @@ -5,27 +5,27 @@ import { import { validateRulesForContext } from "../../../utils/validate-rules" import { AutomationTrigger } from "../../../modules/mpn-automation/types/interfaces" -export interface ValidateAutomationTriggersStepInput { +export interface ValidateTriggersRulesStepInput { triggers: AutomationTrigger[] context: Record } -export const validateAutomationTriggersStepId = - "validate-automation-triggers" +export const validateTriggersRulesStepId = + "validate-triggers-rules" /** * This step validates multiple automation triggers against context data. * * @example - * const data = validateAutomationTriggersStep({ + * const data = validateTriggersRulesStep({ * triggers: [ ... ], * context: { ... } * }) */ -export const validateAutomationTriggersStep = createStep( - validateAutomationTriggersStepId, +export const validateTriggersRulesStep = createStep( + validateTriggersRulesStepId, async ( - input: ValidateAutomationTriggersStepInput + input: ValidateTriggersRulesStepInput ): Promise> => { const { triggers, context } = input diff --git a/src/workflows/mpn-automation/validate-automation-triggers-by-event.ts b/src/workflows/mpn-automation/validate-triggers-by-event.ts similarity index 73% rename from src/workflows/mpn-automation/validate-automation-triggers-by-event.ts rename to src/workflows/mpn-automation/validate-triggers-by-event.ts index eb516f6..017940d 100644 --- a/src/workflows/mpn-automation/validate-automation-triggers-by-event.ts +++ b/src/workflows/mpn-automation/validate-triggers-by-event.ts @@ -5,20 +5,20 @@ import { transform, } from "@medusajs/framework/workflows-sdk" import { getAutomationTriggersByEventStep } from "./steps/retrieve-automation-triggers-by-event" -import { validateAutomationTriggersStep } from "./steps/validate-automation-triggers" +import { validateTriggersRulesStep } from "./steps/validate-triggers-rules" import { TriggerType } from "../../utils/types" import { AutomationTrigger, AutomationAction, } from "../../modules/mpn-automation/types/interfaces" -export interface ValidateAutomationTriggersByEventWorkflowInput { +export interface ValidateTriggersByEventWorkflowInput { eventName: string eventType: TriggerType context: Record } -export interface ValidateAutomationTriggersByEventWorkflowOutput { +export interface ValidateTriggersByEventWorkflowOutput { validated: Array<{ isValid: boolean trigger: AutomationTrigger @@ -27,8 +27,8 @@ export interface ValidateAutomationTriggersByEventWorkflowOutput { triggersCount: number } -export const validateAutomationTriggersByEventWorkflowId = - "validate-automation-triggers-by-event" +export const validateTriggersByEventWorkflowId = + "validate-triggers-by-event" /** * This workflow retrieves notification triggers for an event and validates them against context data. @@ -44,11 +44,11 @@ export const validateAutomationTriggersByEventWorkflowId = * } * }) */ -export const validateAutomationTriggersByEventWorkflow = +export const validateTriggersByEventWorkflow = createWorkflow( - validateAutomationTriggersByEventWorkflowId, + validateTriggersByEventWorkflowId, ( - input: WorkflowData + input: WorkflowData ) => { // Retrieve triggers for the event const getTriggers = getAutomationTriggersByEventStep({ @@ -58,7 +58,7 @@ export const validateAutomationTriggersByEventWorkflow = // Validate all triggers against context const getValidatedTriggers = - validateAutomationTriggersStep({ + validateTriggersRulesStep({ triggers: getTriggers || [], context: input.context, }) diff --git a/src/workflows/order/get-order-by-id.ts b/src/workflows/order/get-order-by-id.ts new file mode 100644 index 0000000..55cf6ca --- /dev/null +++ b/src/workflows/order/get-order-by-id.ts @@ -0,0 +1,34 @@ +import { + createWorkflow, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { getOrderByIdStep } from "./steps/get-order-by-id" + +export interface GetOrderByIdWorkflowInput { + order_id: string +} + +export const getOrderByIdWorkflowId = "get-order-by-id" + +/** + * This workflow retrieves an order by its ID with related items, customer, addresses, and payment collections. + * + * @example + * const { result } = await getOrderByIdWorkflow(container).run({ + * input: { + * order_id: "order_123" + * } + * }) + */ +export const getOrderByIdWorkflow = createWorkflow( + getOrderByIdWorkflowId, + (input: WorkflowData) => { + const order = getOrderByIdStep({ + order_id: input.order_id, + }) + + return new WorkflowResponse(order) + } +) + diff --git a/src/workflows/order/index.ts b/src/workflows/order/index.ts new file mode 100644 index 0000000..2989b32 --- /dev/null +++ b/src/workflows/order/index.ts @@ -0,0 +1,2 @@ +export * from "./get-order-by-id" + diff --git a/src/workflows/order/steps/get-order-by-id.ts b/src/workflows/order/steps/get-order-by-id.ts new file mode 100644 index 0000000..5c77edc --- /dev/null +++ b/src/workflows/order/steps/get-order-by-id.ts @@ -0,0 +1,76 @@ +import type { OrderTypes } from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, + MedusaError, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" +import { ORDER_QUERY_FIELDS } from "../../../modules/mpn-automation/types/modules/order" +import { getFieldsFromAttributes } from "../../../utils" + +export interface GetOrderByIdStepInput { + order_id: string +} + +export interface GetOrderByIdStepOutput { + order: OrderTypes.OrderDTO +} + +export const getOrderByIdStepId = "get-order-by-id" + +/** + * This step retrieves an order by its ID with related items, customer, addresses, and payment collections. + * + * @example + * const data = getOrderByIdStep({ + * order_id: "order_123" + * }) + */ +export const getOrderByIdStep = createStep( + getOrderByIdStepId, + async ( + input: GetOrderByIdStepInput, + { container } + ): Promise> => { + const query = container.resolve( + ContainerRegistrationKeys.QUERY + ) + + if (!input.order_id) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Order ID is required" + ) + } + + // Generate fields from ORDER_QUERY_FIELDS which includes technical relations needed for totals calculation + const fields = getFieldsFromAttributes( + ORDER_QUERY_FIELDS.map((field) => ({ value: field })), + "order" + ) + + const { data: orders } = await query.graph({ + entity: "order", + fields: fields, + filters: { + id: { + $in: [input.order_id], + }, + }, + }) + + if (!orders || orders.length === 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Order with ID ${input.order_id} not found` + ) + } + + return new StepResponse({ + order: orders[0], + }) + } +) + diff --git a/src/workflows/order/steps/index.ts b/src/workflows/order/steps/index.ts new file mode 100644 index 0000000..2989b32 --- /dev/null +++ b/src/workflows/order/steps/index.ts @@ -0,0 +1,2 @@ +export * from "./get-order-by-id" + diff --git a/src/workflows/product/steps/get-product-by-id.ts b/src/workflows/product/steps/get-product-by-id.ts index 228f38d..8ec6a82 100644 --- a/src/workflows/product/steps/get-product-by-id.ts +++ b/src/workflows/product/steps/get-product-by-id.ts @@ -7,7 +7,7 @@ import { StepResponse, createStep, } from "@medusajs/framework/workflows-sdk" -import { PRODUCT_ATTRIBUTES } from "../../../modules/mpn-automation/types/modules/product" +import { PRODUCT_QUERY_FIELDS } from "../../../modules/mpn-automation/types/modules/product" import { getFieldsFromAttributes } from "../../../utils" export interface GetProductByIdStepInput { @@ -45,9 +45,9 @@ export const getProductByIdStep = createStep( ) } - // Generate fields from PRODUCT_ATTRIBUTES to keep them in sync + // Generate fields from PRODUCT_QUERY_FIELDS which includes technical relations needed for complete data retrieval const fields = getFieldsFromAttributes( - PRODUCT_ATTRIBUTES, + PRODUCT_QUERY_FIELDS.map((field) => ({ value: field })), "product" )