From 12e8405ccaa7469cc75f97295cb8d0ba7515b35b Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 15 Nov 2024 18:42:56 +0200 Subject: [PATCH 01/14] docs: add note about using dates in workflows (#10107) --- .../constructor-constraints/page.mdx | 45 ++++++++++++++++++- .../workflows/variable-manipulation/page.mdx | 25 ++++++++++- www/apps/book/generated/edit-dates.mjs | 4 +- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/www/apps/book/app/learn/advanced-development/workflows/constructor-constraints/page.mdx b/www/apps/book/app/learn/advanced-development/workflows/constructor-constraints/page.mdx index 6223860ebacb9..c98b67ef86522 100644 --- a/www/apps/book/app/learn/advanced-development/workflows/constructor-constraints/page.mdx +++ b/www/apps/book/app/learn/advanced-development/workflows/constructor-constraints/page.mdx @@ -34,7 +34,7 @@ You can’t directly manipulate variables within the workflow's constructor func -Learn more about why you can't manipulate variables [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows) +Learn more about why you can't manipulate variables [in this chapter](../variable-manipulation/page.mdx) @@ -79,6 +79,49 @@ const myWorkflow = createWorkflow( }) ``` +### Create Dates in transform + +When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution. + +Instead, create the date using `transform`. + + + +Learn more about how Medusa creates an internal representation of a workflow [in this chapter](../variable-manipulation/page.mdx). + + + +For example: + +export const dateHighlights = [ + ["5", "new Date()", "Don't create a date directly in the constructor function."], + ["16", "transform", "Use the `transform` function to create a date variable."] +] + +```ts highlights={dateHighlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = new Date() + + return new WorkflowResponse({ + today + }) +}) + +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = transform({}, () => new Date()) + + return new WorkflowResponse({ + today + }) +}) +``` + ### No If Conditions You can't use if-conditions in a workflow. diff --git a/www/apps/book/app/learn/advanced-development/workflows/variable-manipulation/page.mdx b/www/apps/book/app/learn/advanced-development/workflows/variable-manipulation/page.mdx index 42a41ce84cafc..d911e3462775e 100644 --- a/www/apps/book/app/learn/advanced-development/workflows/variable-manipulation/page.mdx +++ b/www/apps/book/app/learn/advanced-development/workflows/variable-manipulation/page.mdx @@ -6,7 +6,7 @@ export const metadata = { In this chapter, you'll learn how to use the `transform` utility to manipulate variables in a workflow. -## Why Variable Manipulation isn't Allowed in Workflows? +## Why Variable Manipulation isn't Allowed in Workflows Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. @@ -117,6 +117,29 @@ You then pass the `ids` variable as a parameter to the `doSomethingStep`. --- +## Example: Creating a Date + +If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed. + +So, use `transform` instead to create a date variable with `new Date()`. + +For example: + +```ts +const myWorkflow = createWorkflow( + "hello-world", + () => { + const today = transform({}, () => new Date()) + + doSomethingStep(today) + } +) +``` + +In this workflow, `today` is only evaluated when the workflow is executed. + +--- + ## Caveats ### Transform Evaluation diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 1b0f767355a7c..a13abcbac59ed 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -40,7 +40,7 @@ export const generatedEditDates = { "app/learn/advanced-development/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00", "app/learn/advanced-development/admin/page.mdx": "2024-10-07T12:39:13.178Z", "app/learn/advanced-development/workflows/long-running-workflow/page.mdx": "2024-09-30T08:43:53.129Z", - "app/learn/advanced-development/workflows/constructor-constraints/page.mdx": "2024-10-04T08:40:14.867Z", + "app/learn/advanced-development/workflows/constructor-constraints/page.mdx": "2024-11-14T16:13:19.234Z", "app/learn/advanced-development/data-models/write-migration/page.mdx": "2024-07-15T17:46:10+02:00", "app/learn/advanced-development/data-models/manage-relationships/page.mdx": "2024-09-10T11:39:51.167Z", "app/learn/advanced-development/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", @@ -88,7 +88,7 @@ export const generatedEditDates = { "app/learn/debugging-and-testing/instrumentation/page.mdx": "2024-09-17T08:53:15.910Z", "app/learn/advanced-development/api-routes/additional-data/page.mdx": "2024-09-30T08:43:53.120Z", "app/learn/advanced-development/workflows/page.mdx": "2024-09-18T08:00:57.364Z", - "app/learn/advanced-development/workflows/variable-manipulation/page.mdx": "2024-11-11T13:33:41.270Z", + "app/learn/advanced-development/workflows/variable-manipulation/page.mdx": "2024-11-14T16:11:24.538Z", "app/learn/customization/custom-features/api-route/page.mdx": "2024-09-12T12:42:34.201Z", "app/learn/customization/custom-features/module/page.mdx": "2024-10-16T08:49:44.676Z", "app/learn/customization/custom-features/workflow/page.mdx": "2024-09-30T08:43:53.133Z", From 1fb2998620788e68fb343023bbd4a727ab6b1c3d Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 15 Nov 2024 18:54:33 +0200 Subject: [PATCH 02/14] docs: document nested UI routes (#10104) * docs: document nested UI routes * address PR feedback --- .../admin/ui-routes/page.mdx | 35 +++++++++++++++++++ www/apps/book/generated/edit-dates.mjs | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/www/apps/book/app/learn/advanced-development/admin/ui-routes/page.mdx b/www/apps/book/app/learn/advanced-development/admin/ui-routes/page.mdx index a16b7a4971445..0815a2f4d34c0 100644 --- a/www/apps/book/app/learn/advanced-development/admin/ui-routes/page.mdx +++ b/www/apps/book/app/learn/advanced-development/admin/ui-routes/page.mdx @@ -99,6 +99,41 @@ The configuration object is created using the `defineRouteConfig` function impor The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](!ui!/icons/overview). +### Nested UI Routes + +Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: + +```tsx title="src/admin/routes/custom/nested/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const NestedCustomPage = () => { + return ( + +
+ This is my nested custom route +
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Nested Route", +}) + +export default NestedCustomPage +``` + +This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked. + +#### Caveats + +Some caveats for nested UI routes in the sidebar: + +- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. +- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. +- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. + --- ## Create Settings Page diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index a13abcbac59ed..7e1bf2ba01171 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -56,7 +56,7 @@ export const generatedEditDates = { "app/learn/advanced-development/api-routes/http-methods/page.mdx": "2024-09-11T10:43:33.169Z", "app/learn/advanced-development/admin/tips/page.mdx": "2024-10-07T12:50:36.335Z", "app/learn/advanced-development/api-routes/cors/page.mdx": "2024-09-30T08:43:53.121Z", - "app/learn/advanced-development/admin/ui-routes/page.mdx": "2024-10-07T12:52:37.509Z", + "app/learn/advanced-development/admin/ui-routes/page.mdx": "2024-11-14T15:29:22.901Z", "app/learn/advanced-development/api-routes/middlewares/page.mdx": "2024-09-11T10:45:31.861Z", "app/learn/advanced-development/modules/isolation/page.mdx": "2024-07-04T17:26:03+03:00", "app/learn/advanced-development/data-models/configure-properties/page.mdx": "2024-09-30T08:43:53.122Z", From b0c6efa98b02e214c3d686bab882bdec6d9189af Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 15 Nov 2024 18:59:46 +0200 Subject: [PATCH 03/14] docs: fix copy in code blocks with tabs (#10123) * docs: fix copy in code blocks with tabs * fix lint error --- .../docs-ui/src/components/CodeTabs/index.tsx | 50 +++++++++++++++---- .../src/components/Npm2YarnCode/index.tsx | 1 + 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/www/packages/docs-ui/src/components/CodeTabs/index.tsx b/www/packages/docs-ui/src/components/CodeTabs/index.tsx index 35df69d0118f0..20e6d3e7891a9 100644 --- a/www/packages/docs-ui/src/components/CodeTabs/index.tsx +++ b/www/packages/docs-ui/src/components/CodeTabs/index.tsx @@ -53,6 +53,36 @@ export const CodeTabs = ({ return "source" in typedProps } + const getCodeBlockProps = ( + codeBlock: React.ReactElement< + unknown, + string | React.JSXElementConstructor + > + ): CodeBlockProps | undefined => { + if (typeof codeBlock.props !== "object" || !codeBlock.props) { + return undefined + } + + if ("source" in codeBlock.props) { + return codeBlock.props as CodeBlockProps + } + + if ( + "children" in codeBlock.props && + typeof codeBlock.props.children === "object" && + codeBlock.props.children + ) { + return getCodeBlockProps( + codeBlock.props.children as React.ReactElement< + unknown, + string | React.JSXElementConstructor + > + ) + } + + return undefined + } + const tabs: CodeTab[] = useMemo(() => { const tempTabs: CodeTab[] = [] Children.forEach(children, (child) => { @@ -79,10 +109,19 @@ export const CodeTabs = ({ const codeBlockProps = codeBlock.props as CodeBlockProps + const modifiedProps: CodeBlockProps = { + ...(getCodeBlockProps(codeBlock) || { + source: "", + }), + badgeLabel: undefined, + hasTabs: true, + className: clsx("!my-0", codeBlockProps.className), + } + tempTabs.push({ label: typedChildProps.label, value: typedChildProps.value, - codeProps: codeBlockProps, + codeProps: modifiedProps, codeBlock: { ...codeBlock, props: { @@ -91,14 +130,7 @@ export const CodeTabs = ({ ...(typeof codeBlockProps.children === "object" ? codeBlockProps.children : {}), - props: { - ...(React.isValidElement(codeBlockProps.children) - ? (codeBlockProps.children.props as Record) - : {}), - badgeLabel: undefined, - hasTabs: true, - className: clsx("!my-0", codeBlockProps.className), - }, + props: modifiedProps, }, }, }, diff --git a/www/packages/docs-ui/src/components/Npm2YarnCode/index.tsx b/www/packages/docs-ui/src/components/Npm2YarnCode/index.tsx index 1592c6f678510..6ff09ede8b30d 100644 --- a/www/packages/docs-ui/src/components/Npm2YarnCode/index.tsx +++ b/www/packages/docs-ui/src/components/Npm2YarnCode/index.tsx @@ -13,6 +13,7 @@ export const Npm2YarnCode = ({ npmCode, ...rest }: Npm2YarnCodeProps) => { const lang = "bash" const { title = "", ...codeOptions } = rest + codeOptions.hasTabs = true const tabs = [ { From 1d88ad37932b8757b044640f72e264f31dd94456 Mon Sep 17 00:00:00 2001 From: Mehmet Erturk <30368068+ertyurk@users.noreply.github.com> Date: Fri, 15 Nov 2024 23:12:34 +0100 Subject: [PATCH 04/14] feat: Add Turkish Language (#10109) Turkish translation added. --- .../admin/dashboard/src/i18n/languages.ts | 8 +- .../dashboard/src/i18n/translations/index.ts | 4 + .../dashboard/src/i18n/translations/tr.json | 2755 +++++++++++++++++ 3 files changed, 2766 insertions(+), 1 deletion(-) create mode 100644 packages/admin/dashboard/src/i18n/translations/tr.json diff --git a/packages/admin/dashboard/src/i18n/languages.ts b/packages/admin/dashboard/src/i18n/languages.ts index b78f4b3fff48e..214ea5f120582 100644 --- a/packages/admin/dashboard/src/i18n/languages.ts +++ b/packages/admin/dashboard/src/i18n/languages.ts @@ -1,4 +1,4 @@ -import { de, enUS, pl } from "date-fns/locale" +import { de, enUS, pl, tr } from "date-fns/locale" import { Language } from "./types" export const languages: Language[] = [ @@ -20,4 +20,10 @@ export const languages: Language[] = [ ltr: true, date_locale: pl, }, + { + code: "tr", + display_name: "Türkçe", + ltr: true, + date_locale: tr, + }, ] diff --git a/packages/admin/dashboard/src/i18n/translations/index.ts b/packages/admin/dashboard/src/i18n/translations/index.ts index 21eecb82bcf1a..e71d4f35d846b 100644 --- a/packages/admin/dashboard/src/i18n/translations/index.ts +++ b/packages/admin/dashboard/src/i18n/translations/index.ts @@ -1,6 +1,7 @@ import de from "./de.json" import en from "./en.json" import pl from "./pl.json" +import tr from "./tr.json" export default { en: { @@ -12,4 +13,7 @@ export default { pl: { translation: pl, }, + tr: { + translation: tr, + }, } diff --git a/packages/admin/dashboard/src/i18n/translations/tr.json b/packages/admin/dashboard/src/i18n/translations/tr.json new file mode 100644 index 0000000000000..9812581eb797a --- /dev/null +++ b/packages/admin/dashboard/src/i18n/translations/tr.json @@ -0,0 +1,2755 @@ +{ + "$schema": "./$schema.json", + "general": { + "ascending": "Artan", + "descending": "Azalan", + "add": "Ekle", + "start": "Başla", + "end": "Bitiş", + "open": "Aç", + "close": "Kapat", + "apply": "Uygula", + "range": "Aralık", + "search": "Ara", + "of": "arasında", + "results": "sonuçlar", + "pages": "sayfalar", + "next": "Sonraki", + "prev": "Önceki", + "is": "olduğunda", + "timeline": "Zaman Çizelgesi", + "success": "Başarılı", + "warning": "Uyarı", + "tip": "İpucu", + "error": "Hata", + "select": "Seç", + "selected": "Seçildi", + "enabled": "Etkin", + "disabled": "Devre Dışı", + "expired": "Süresi Dolmuş", + "active": "Aktif", + "revoked": "İptal Edildi", + "new": "Yeni", + "modified": "Değiştirildi", + "added": "Eklendi", + "removed": "Kaldırıldı", + "admin": "Yönetici", + "store": "Mağaza", + "details": "Detaylar", + "items_one": "{{count}} ürün", + "items_other": "{{count}} ürün", + "countSelected": "{{count}} seçildi", + "countOfTotalSelected": "{{total}} arasından {{count}} seçildi", + "plusCount": "+ {{count}}", + "plusCountMore": "+ {{count}} daha fazla", + "areYouSure": "Emin misiniz?", + "noRecordsFound": "Kayıt bulunamadı", + "typeToConfirm": "{val} yazın ve onaylayın:", + "noResultsTitle": "Sonuç Yok", + "noResultsMessage": "Filtreleri veya arama sorgusunu değiştirmeyi deneyin", + "noSearchResults": "Arama sonucu yok", + "noSearchResultsFor": "<0>'{{query}}' için sonuç yok", + "noRecordsTitle": "Kayıt Yok", + "noRecordsMessage": "Gösterilecek kayıt yok", + "unsavedChangesTitle": "Bu formdan çıkmak istediğinizden emin misiniz?", + "unsavedChangesDescription": "Kaydedilmemiş değişiklikleriniz kaybolacak.", + "includesTaxTooltip": "Bu sütundaki fiyatlar vergiler dahil fiyatlardır.", + "excludesTaxTooltip": "Bu sütundaki fiyatlar vergiler hariç fiyatlardır.", + "noMoreData": "Daha fazla veri yok" + }, + "json": { + "header": "JSON", + "numberOfKeys_one": "{{count}} anahtar", + "numberOfKeys_other": "{{count}} anahtar", + "drawer": { + "header_one": "JSON <0>· {{count}} anahtar", + "header_other": "JSON <0>· {{count}} anahtar", + "description": "Bu nesnenin JSON verilerini görüntüleyin." + } + }, + "metadata": { + "header": "Meta Veriler", + "numberOfKeys_one": "{{count}} anahtar", + "numberOfKeys_other": "{{count}} anahtar", + "edit": { + "header": "Meta Verileri Düzenle", + "description": "Bu nesnenin meta verilerini düzenleyin.", + "successToast": "Meta veriler başarıyla güncellendi.", + "actions": { + "insertRowAbove": "Üstüne satır ekle", + "insertRowBelow": "Altına satır ekle", + "deleteRow": "Satırı sil" + }, + "labels": { + "key": "Anahtar", + "value": "Değer" + }, + "complexRow": { + "label": "Bazı satırlar devre dışı", + "description": "Bu nesne, düzenlenemeyen dizi veya nesneler gibi ilkel olmayan meta veriler içeriyor. Devre dışı satırları düzenlemek için API'yi kullanın.", + "tooltip": "Bu satır, ilkel olmayan veri içerdiği için devre dışıdır." + } + } + }, + "validation": { + "mustBeInt": "Değer tam sayı olmalıdır.", + "mustBePositive": "Değer pozitif bir sayı olmalıdır." + }, + "actions": { + "save": "Kaydet", + "saveAsDraft": "Taslak olarak kaydet", + "copy": "Kopyala", + "copied": "Kopyalandı", + "duplicate": "Çoğalt", + "publish": "Yayınla", + "create": "Oluştur", + "delete": "Sil", + "remove": "Kaldır", + "revoke": "İptal Et", + "cancel": "İptal", + "forceConfirm": "Zorla Onayla", + "continueEdit": "Düzenlemeye Devam Et", + "enable": "Etkinleştir", + "disable": "Devre Dışı Bırak", + "undo": "Geri Al", + "complete": "Tamamla", + "viewDetails": "Detayları Görüntüle", + "back": "Geri", + "close": "Kapat", + "showMore": "Daha Fazla Göster", + "continue": "Devam Et", + "continueWithEmail": "E-posta ile Devam Et", + "idCopiedToClipboard": "ID panoya kopyalandı", + "addReason": "Sebep Ekle", + "addNote": "Not Ekle", + "reset": "Sıfırla", + "confirm": "Onayla", + "edit": "Düzenle", + "addItems": "Ürünler Ekle", + "download": "İndir", + "clear": "Temizle", + "clearAll": "Tümünü Temizle", + "apply": "Uygula", + "add": "Ekle", + "select": "Seç", + "browse": "Gözat", + "logout": "Çıkış Yap", + "hide": "Gizle", + "export": "Dışa Aktar", + "import": "İçe Aktar" + }, + "operators": { + "in": "İçinde" + }, + "app": { + "search": { + "label": "Arama", + "title": "Arama", + "description": "Tüm mağazanızda siparişler, ürünler, müşteriler ve daha fazlasını arayın.", + "allAreas": "Tüm Alanlar", + "navigation": "Navigasyon", + "openResult": "Sonucu Aç", + "showMore": "Daha Fazla Göster", + "placeholder": "Bir şeye atlayın veya bir şey bulun...", + "noResultsTitle": "Sonuç bulunamadı", + "noResultsMessage": "Aramanıza uyan bir şey bulamadık.", + "emptySearchTitle": "Aramak için yazın", + "emptySearchMessage": "Keşfetmek için bir anahtar kelime veya ifade girin.", + "loadMore": "{{count}} daha yükle", + "groups": { + "all": "Tüm Alanlar", + "customer": "Müşteriler", + "customerGroup": "Müşteri Grupları", + "product": "Ürünler", + "productVariant": "Ürün Varyantları", + "inventory": "Envanter", + "reservation": "Rezervasyonlar", + "category": "Kategoriler", + "collection": "Koleksiyonlar", + "order": "Siparişler", + "promotion": "Promosyonlar", + "campaign": "Kampanyalar", + "priceList": "Fiyat Listeleri", + "user": "Kullanıcılar", + "region": "Bölgeler", + "taxRegion": "Vergi Bölgeleri", + "returnReason": "İade Nedenleri", + "salesChannel": "Satış Kanalları", + "productType": "Ürün Tipleri", + "productTag": "Ürün Etiketleri", + "location": "Konumlar", + "shippingProfile": "Kargo Profilleri", + "publishableApiKey": "Yayınlanabilir API Anahtarları", + "secretApiKey": "Gizli API Anahtarları", + "command": "Komutlar", + "navigation": "Navigasyon" + } + }, + "keyboardShortcuts": { + "pageShortcut": "Sayfaya Git", + "settingShortcut": "Ayarlar", + "commandShortcut": "Komutlar", + "then": "sonra", + "navigation": { + "goToOrders": "Siparişler", + "goToProducts": "Ürünler", + "goToCollections": "Koleksiyonlar", + "goToCategories": "Kategoriler", + "goToCustomers": "Müşteriler", + "goToCustomerGroups": "Müşteri Grupları", + "goToInventory": "Envanter", + "goToReservations": "Rezervasyonlar", + "goToPriceLists": "Fiyat Listeleri", + "goToPromotions": "Promosyonlar", + "goToCampaigns": "Kampanyalar" + }, + "settings": { + "goToSettings": "Ayarlar", + "goToStore": "Mağaza", + "goToUsers": "Kullanıcılar", + "goToRegions": "Bölgeler", + "goToTaxRegions": "Vergi Bölgeleri", + "goToSalesChannels": "Satış Kanalları", + "goToProductTypes": "Ürün Tipleri", + "goToLocations": "Konumlar", + "goToPublishableApiKeys": "Yayınlanabilir API Anahtarları", + "goToSecretApiKeys": "Gizli API Anahtarları", + "goToWorkflows": "İş Akışları", + "goToProfile": "Profil", + "goToReturnReasons": "İade Nedenleri" + } + }, + "menus": { + "user": { + "documentation": "Dokümantasyon", + "changelog": "Güncellemeler", + "shortcuts": "Kısayollar", + "profileSettings": "Profil Ayarları", + "theme": { + "label": "Tema", + "dark": "Karanlık", + "light": "Aydınlık", + "system": "Sistem" + } + }, + "store": { + "label": "Mağaza", + "storeSettings": "Mağaza Ayarları" + }, + "actions": { + "logout": "Çıkış Yap" + } + }, + "nav": { + "accessibility": { + "title": "Navigasyon", + "description": "Gösterge paneli için navigasyon menüsü." + }, + "common": { + "extensions": "Eklentiler" + }, + "main": { + "store": "Mağaza", + "storeSettings": "Mağaza Ayarları" + }, + "settings": { + "header": "Ayarlar", + "general": "Genel", + "developer": "Geliştirici", + "myAccount": "Hesabım" + } + } + }, + "dataGrid": { + "columns": { + "view": "Görüntüle", + "resetToDefault": "Varsayılana Sıfırla", + "disabled": "Görüntülenen sütunların değiştirilmesi devre dışı bırakılmıştır." + }, + "shortcuts": { + "label": "Kısayollar", + "commands": { + "undo": "Geri Al", + "redo": "Yinele", + "copy": "Kopyala", + "paste": "Yapıştır", + "edit": "Düzenle", + "delete": "Sil", + "clear": "Temizle", + "moveUp": "Yukarı Taşı", + "moveDown": "Aşağı Taşı", + "moveLeft": "Sola Taşı", + "moveRight": "Sağa Taşı", + "moveTop": "En Üste Taşı", + "moveBottom": "En Alta Taşı", + "selectDown": "Aşağı Seç", + "selectUp": "Yukarı Seç", + "selectColumnDown": "Sütunu Aşağı Seç", + "selectColumnUp": "Sütunu Yukarı Seç", + "focusToolbar": "Araç Çubuğuna Odaklan", + "focusCancel": "Odak İptal" + } + }, + "errors": { + "fixError": "Hatasını Düzelt", + "count_one": "{{count}} hata", + "count_other": "{{count}} hata" + } + }, + "filters": { + "date": { + "today": "Bugün", + "lastSevenDays": "Son 7 gün", + "lastThirtyDays": "Son 30 gün", + "lastNinetyDays": "Son 90 gün", + "lastTwelveMonths": "Son 12 ay", + "custom": "Özel", + "from": "Başlangıç", + "to": "Bitiş" + }, + "compare": { + "lessThan": "Küçüktür", + "greaterThan": "Büyüktür", + "exact": "Tam", + "range": "Aralık", + "lessThanLabel": "{{value}}'den küçük", + "greaterThanLabel": "{{value}}'den büyük", + "andLabel": "ve" + }, + "addFilter": "Filtre Ekle" + }, + "errorBoundary": { + "badRequestTitle": "400 - Hatalı İstek", + "badRequestMessage": "Sunucu, hatalı bir sözdizimi nedeniyle isteği anlayamadı.", + "notFoundTitle": "404 - Bu adreste bir sayfa yok", + "notFoundMessage": "URL'yi kontrol edin ve tekrar deneyin veya arama çubuğunu kullanarak aradığınız şeyi bulun.", + "internalServerErrorTitle": "500 - Dahili Sunucu Hatası", + "internalServerErrorMessage": "Sunucuda beklenmeyen bir hata oluştu. Lütfen daha sonra tekrar deneyin.", + "defaultTitle": "Bir hata oluştu", + "defaultMessage": "Bu sayfa oluşturulurken beklenmeyen bir hata meydana geldi.", + "noMatchMessage": "Aradığınız sayfa mevcut değil.", + "backToDashboard": "Gösterge Paneline Dön" + }, + "addresses": { + "shippingAddress": { + "header": "Teslimat Adresi", + "editHeader": "Teslimat Adresini Düzenle", + "editLabel": "Teslimat adresi", + "label": "Teslimat adresi" + }, + "billingAddress": { + "header": "Fatura Adresi", + "editHeader": "Fatura Adresini Düzenle", + "editLabel": "Fatura adresi", + "label": "Fatura adresi", + "sameAsShipping": "Teslimat adresi ile aynı" + }, + "contactHeading": "İletişim", + "locationHeading": "Konum" + }, + "email": { + "editHeader": "E-postayı Düzenle", + "editLabel": "E-posta", + "label": "E-posta" + }, + "transferOwnership": { + "header": "Sahipliği Aktar", + "label": "Sahipliği aktar", + "details": { + "order": "Sipariş detayları", + "draft": "Taslak detayları" + }, + "currentOwner": { + "label": "Mevcut sahip", + "hint": "Siparişin mevcut sahibi." + }, + "newOwner": { + "label": "Yeni sahip", + "hint": "Siparişin aktarılacağı yeni sahip." + }, + "validation": { + "mustBeDifferent": "Yeni sahip, mevcut sahipten farklı olmalıdır.", + "required": "Yeni sahip zorunludur." + } + }, + "sales_channels": { + "availableIn": "<0>{{x}} satış kanalından <1>{{y}} tanesinde mevcut" + }, + "products": { + "domain": "Ürünler", + "list": { + "noRecordsMessage": "Satışa başlamak için ilk ürününüzü oluşturun." + }, + "edit": { + "header": "Ürünü Düzenle", + "description": "Ürün detaylarını düzenleyin.", + "successToast": "Ürün {{title}} başarıyla güncellendi." + }, + "create": { + "header": "Genel", + "tabs": { + "details": "Detaylar", + "organize": "Organize Et", + "variants": "Varyantlar", + "inventory": "Envanter Kitleri" + }, + "errors": { + "variants": "Lütfen en az bir varyant seçin.", + "options": "Lütfen en az bir seçenek oluşturun.", + "uniqueSku": "SKU benzersiz olmalıdır." + }, + "inventory": { + "heading": "Envanter Kitleri", + "label": "Varyantın envanter kitine envanter öğeleri ekleyin.", + "itemPlaceholder": "Envanter öğesi seçin", + "quantityPlaceholder": "Kit için kaç tane gerektiğini belirtin." + }, + "variants": { + "header": "Varyantlar", + "subHeadingTitle": "Evet, bu bir varyantlı ürün", + "subHeadingDescription": "İşaretlenmediğinde, sizin için varsayılan bir varyant oluşturacağız", + "optionTitle": { + "placeholder": "Beden" + }, + "optionValues": { + "placeholder": "Küçük, Orta, Büyük" + }, + "productVariants": { + "label": "Ürün varyantları", + "hint": "Bu sıralama, varyantların mağazanızdaki sırasını etkileyecektir.", + "alert": "Varyantlar oluşturmak için seçenekler ekleyin.", + "tip": "İşaretlenmeyen varyantlar oluşturulmayacaktır. Ürün seçeneklerinizdeki varyasyonları bu listeye uyacak şekilde oluşturup düzenleyebilirsiniz." + }, + "productOptions": { + "label": "Ürün seçenekleri", + "hint": "Ürün için seçenekleri belirleyin, örneğin renk, boyut vb." + } + }, + "successToast": "Ürün {{title}} başarıyla oluşturuldu." + }, + "export": { + "header": "Ürün Listesini Dışa Aktar", + "description": "Ürün listesini bir CSV dosyasına dışa aktarın.", + "success": { + "title": "Dışa aktarma işleminiz işleniyor", + "description": "Veri dışa aktarımı birkaç dakika sürebilir. İşlem tamamlandığında size bildireceğiz." + }, + "filters": { + "title": "Filtreler", + "description": "Bu görünümü ayarlamak için tablo görünümünde filtreleri uygulayın" + }, + "columns": { + "title": "Sütunlar", + "description": "Belirli ihtiyaçları karşılamak için dışa aktarılan verileri özelleştirin" + } + }, + "import": { + "header": "Ürün Listesini İçe Aktar", + "uploadLabel": "Ürünleri İçe Aktar", + "uploadHint": "Bir CSV dosyasını sürükleyip bırakın veya yüklemek için tıklayın", + "description": "Ürünleri, önceden tanımlanmış bir formatta bir CSV dosyası sağlayarak içe aktarın", + "template": { + "title": "Listenizi nasıl düzenleyeceğinizden emin değil misiniz?", + "description": "Doğru formatı takip ettiğinizden emin olmak için aşağıdaki şablonu indirin." + }, + "upload": { + "title": "Bir CSV dosyası yükleyin", + "description": "İçe aktarma yoluyla ürün ekleyebilir veya güncelleyebilirsiniz. Mevcut ürünleri güncellemek için mevcut tutamaç ve ID'yi, mevcut varyantları güncellemek için mevcut ID'yi kullanmanız gerekmektedir. Ürünleri içe aktarmadan önce sizden onay istenecektir.", + "preprocessing": "Ön işleme...", + "productsToCreate": "Ürünler oluşturulacak", + "productsToUpdate": "Ürünler güncellenecek" + }, + "success": { + "title": "İçe aktarma işleminiz işleniyor", + "description": "Veri içe aktarma bir süre alabilir. İşlem tamamlandığında size bildireceğiz." + } + }, + "deleteWarning": "Ürün {{title}}'i silmek üzeresiniz. Bu işlem geri alınamaz.", + "variants": "Varyantlar", + "attributes": "Öznitelikler", + "editAttributes": "Öznitelikleri Düzenle", + "editOptions": "Seçenekleri Düzenle", + "editPrices": "Fiyatları düzenle", + "media": { + "label": "Medya", + "editHint": "Ürünü mağazanızda sergilemek için medya ekleyin.", + "makeThumbnail": "Küçük resim yap", + "uploadImagesLabel": "Resimleri yükle", + "uploadImagesHint": "Resimleri buraya sürükleyip bırakın veya yüklemek için tıklayın.", + "invalidFileType": "'{{name}}' desteklenmeyen bir dosya türüdür. Desteklenen dosya türleri: {{types}}.", + "failedToUpload": "Eklenen medya yüklenemedi. Lütfen tekrar deneyin.", + "deleteWarning_one": "{{count}} resmi silmek üzeresiniz. Bu işlem geri alınamaz.", + "deleteWarning_other": "{{count}} resmi silmek üzeresiniz. Bu işlem geri alınamaz.", + "deleteWarningWithThumbnail_one": "Küçük resim dahil {{count}} resmi silmek üzeresiniz. Bu işlem geri alınamaz.", + "deleteWarningWithThumbnail_other": "Küçük resim dahil {{count}} resmi silmek üzeresiniz. Bu işlem geri alınamaz.", + "thumbnailTooltip": "Küçük resim", + "galleryLabel": "Galeri", + "downloadImageLabel": "Mevcut resmi indir", + "deleteImageLabel": "Mevcut resmi sil", + "emptyState": { + "header": "Henüz medya yok", + "description": "Ürünü mağazanızda sergilemek için medya ekleyin.", + "action": "Medya ekle" + } + }, + "discountableHint": "İşaretlenmediğinde, bu ürüne indirim uygulanmayacaktır.", + "noSalesChannels": "Hiçbir satış kanalında mevcut değil", + "variantCount_one": "{{count}} varyant", + "variantCount_other": "{{count}} varyant", + "deleteVariantWarning": "Varyant {{title}}'i silmek üzeresiniz. Bu işlem geri alınamaz.", + "productStatus": { + "draft": "Taslak", + "published": "Yayınlandı", + "proposed": "Önerildi", + "rejected": "Reddedildi" + }, + "fields": { + "title": { + "label": "Başlık", + "hint": "Ürününüz için kısa ve net bir başlık verin.<0/> Arama motorları için önerilen uzunluk 50-60 karakterdir." + }, + "subtitle": { + "label": "Alt Başlık" + }, + "handle": { + "label": "Tutamak", + "tooltip": "Tutamak, ürünü mağazanızda referans almak için kullanılır. Belirtilmediğinde, ürün başlığından oluşturulacaktır." + }, + "description": { + "label": "Açıklama", + "hint": "Ürününüz için kısa ve net bir açıklama verin.<0/> Arama motorları için önerilen uzunluk 120-160 karakterdir." + }, + "discountable": { + "label": "İndirim Uygulanabilir", + "hint": "İşaretlenmediğinde, bu ürüne indirim uygulanmaz." + }, + "type": { + "label": "Tür" + }, + "collection": { + "label": "Koleksiyon" + }, + "categories": { + "label": "Kategoriler" + }, + "tags": { + "label": "Etiketler" + }, + "sales_channels": { + "label": "Satış kanalları", + "hint": "Bu ürün yalnızca varsayılan satış kanalında mevcut olacaktır." + }, + "countryOrigin": { + "label": "Menşe Ülke" + }, + "material": { + "label": "Malzeme" + }, + "width": { + "label": "Genişlik" + }, + "length": { + "label": "Uzunluk" + }, + "height": { + "label": "Yükseklik" + }, + "weight": { + "label": "Ağırlık" + }, + "options": { + "label": "Ürün seçenekleri", + "hint": "Ürünün rengini, boyutunu vb. tanımlamak için kullanılır.", + "add": "Seçenek ekle", + "optionTitle": "Seçenek başlığı", + "optionTitlePlaceholder": "Renk", + "variations": "Varyasyonlar (virgülle ayrılmış)", + "variantionsPlaceholder": "Kırmızı, Mavi, Yeşil" + }, + "variants": { + "label": "Ürün varyantları", + "hint": "İşaretlenmeyen varyantlar oluşturulmayacaktır. Bu sıralama varyantların ön uçtaki sıralamasını etkiler." + }, + "mid_code": { + "label": "Mid kodu" + }, + "hs_code": { + "label": "HS kodu" + } + }, + "variant": { + "edit": { + "header": "Varyantı Düzenle", + "success": "Ürün varyantı başarıyla düzenlendi" + }, + "create": { + "header": "Varyant detayları" + }, + "deleteWarning": "Bu varyantı silmek istediğinizden emin misiniz?", + "pricesPagination": "1 - {{current}} / {{total}} fiyatlar", + "tableItemAvailable": "{{availableCount}} mevcut", + "tableItem_one": "{{availableCount}} öğesi {{locationCount}} konumda mevcut", + "tableItem_other": "{{availableCount}} öğesi {{locationCount}} konumda mevcut", + "inventory": { + "notManaged": "Yönetilmiyor", + "manageItems": "Envanter öğelerini yönet", + "notManagedDesc": "Bu varyant için envanter yönetilmiyor. Envanteri izlemek için 'Envanteri Yönet' özelliğini açın.", + "manageKit": "Envanter kitini yönet", + "navigateToItem": "Envanter öğesine git", + "actions": { + "inventoryItems": "Envanter öğesine git", + "inventoryKit": "Envanter öğelerini göster" + }, + "inventoryKit": "Envanter Kiti", + "inventoryKitHint": "Bu varyant birkaç envanter öğesinden mi oluşuyor?", + "validation": { + "itemId": "Lütfen envanter öğesini seçin.", + "quantity": "Miktar zorunludur. Lütfen pozitif bir sayı girin." + }, + "header": "Stok & Envanter", + "editItemDetails": "Öğe detaylarını düzenle", + "manageInventoryLabel": "Envanteri yönet", + "manageInventoryHint": "Etkinleştirildiğinde, siparişler ve iadeler oluşturulduğunda envanter miktarını sizin için değiştireceğiz.", + "allowBackordersLabel": "Geri siparişlere izin ver", + "allowBackordersHint": "Etkinleştirildiğinde, müşteriler mevcut miktar olmasa bile varyantı satın alabilir.", + "toast": { + "levelsBatch": "Envanter seviyeleri güncellendi.", + "update": "Envanter öğesi başarıyla güncellendi.", + "updateLevel": "Envanter seviyesi başarıyla güncellendi.", + "itemsManageSuccess": "Envanter öğeleri başarıyla güncellendi." + } + } + }, + "options": { + "header": "Seçenekler", + "edit": { + "header": "Seçeneği Düzenle", + "successToast": "Seçenek {{title}} başarıyla güncellendi." + }, + "create": { + "header": "Seçenek Oluştur", + "successToast": "Seçenek {{title}} başarıyla oluşturuldu." + }, + "deleteWarning": "Ürün seçeneğini silmek üzeresiniz: {{title}}. Bu işlem geri alınamaz." + }, + "organization": { + "header": "Organize Et", + "edit": { + "header": "Organizasyonu Düzenle", + "toasts": { + "success": "{{title}}'in organizasyonu başarıyla güncellendi." + } + } + }, + "toasts": { + "delete": { + "success": { + "header": "Ürün silindi", + "description": "{{title}} başarıyla silindi." + }, + "error": { + "header": "Ürün silinemedi" + } + } + } + }, + "collections": { + "domain": "Koleksiyonlar", + "subtitle": "Ürünleri koleksiyonlar halinde organize edin.", + "createCollection": "Koleksiyon Oluştur", + "createCollectionHint": "Ürünlerinizi organize etmek için yeni bir koleksiyon oluşturun.", + "createSuccess": "Koleksiyon başarıyla oluşturuldu.", + "editCollection": "Koleksiyonu Düzenle", + "handleTooltip": "Tutamak, koleksiyonu mağazanızda referans almak için kullanılır. Belirtilmezse, koleksiyon başlığından oluşturulacaktır.", + "deleteWarning": "Koleksiyon {{title}}'i silmek üzeresiniz. Bu işlem geri alınamaz.", + "removeSingleProductWarning": "Ürünü koleksiyondan kaldırmak üzeresiniz: {{title}}. Bu işlem geri alınamaz.", + "removeProductsWarning_one": "{{count}} ürünü koleksiyondan kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "removeProductsWarning_other": "{{count}} ürünü koleksiyondan kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "products": { + "list": { + "noRecordsMessage": "Koleksiyonda ürün yok." + }, + "add": { + "successToast_one": "Ürün koleksiyona başarıyla eklendi.", + "successToast_other": "Ürünler koleksiyona başarıyla eklendi." + }, + "remove": { + "successToast_one": "Ürün koleksiyondan başarıyla kaldırıldı.", + "successToast_other": "Ürünler koleksiyondan başarıyla kaldırıldı." + } + } + }, + "categories": { + "domain": "Kategoriler", + "subtitle": "Ürünleri kategoriler halinde organize edin ve bu kategorilerin sıralama ve hiyerarşisini yönetin.", + "create": { + "header": "Kategori Oluştur", + "hint": "Ürünlerinizi organize etmek için yeni bir kategori oluşturun.", + "tabs": { + "details": "Detaylar", + "organize": "Sıralamayı Düzenle" + }, + "successToast": "Kategori {{name}} başarıyla oluşturuldu." + }, + "edit": { + "header": "Kategoriyi Düzenle", + "description": "Kategoriyi düzenleyerek detaylarını güncelleyin.", + "successToast": "Kategori başarıyla güncellendi." + }, + "delete": { + "confirmation": "Kategori {{name}}'i silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "Kategori {{name}} başarıyla silindi." + }, + "products": { + "add": { + "disabledTooltip": "Ürün zaten bu kategoride.", + "successToast_one": "{{count}} ürün kategoriye eklendi.", + "successToast_other": "{{count}} ürün kategoriye eklendi." + }, + "remove": { + "confirmation_one": "{{count}} ürünü kategoriden kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "confirmation_other": "{{count}} ürünü kategoriden kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "successToast_one": "{{count}} ürün kategoriden kaldırıldı.", + "successToast_other": "{{count}} ürün kategoriden kaldırıldı." + }, + "list": { + "noRecordsMessage": "Kategoride ürün yok." + } + }, + "organize": { + "header": "Organize Et", + "action": "Sıralamayı Düzenle" + }, + "fields": { + "visibility": { + "label": "Görünürlük", + "internal": "Dahili", + "public": "Genel" + }, + "status": { + "label": "Durum", + "active": "Aktif", + "inactive": "Pasif" + }, + "path": { + "label": "Yol", + "tooltip": "Kategorinin tam yolunu göster." + }, + "children": { + "label": "Alt Kategoriler" + }, + "new": { + "label": "Yeni" + } + } + }, + "inventory": { + "domain": "Envanter", + "subtitle": "Envanter öğelerinizi yönetin", + "reserved": "Rezerve Edildi", + "available": "Mevcut", + "locationLevels": "Konumlar", + "associatedVariants": "İlişkili varyantlar", + "manageLocations": "Konumları yönet", + "deleteWarning": "Bir envanter öğesini silmek üzeresiniz. Bu işlem geri alınamaz.", + "editItemDetails": "Öğe detaylarını düzenle", + "create": { + "title": "Envanter Öğesi Oluştur", + "details": "Detaylar", + "availability": "Mevcutluk", + "locations": "Konumlar", + "attributes": "Öznitelikler", + "requiresShipping": "Kargo Gerektirir", + "requiresShippingHint": "Envanter öğesi kargo gerektiriyor mu?", + "successToast": "Envanter öğesi başarıyla oluşturuldu." + }, + "reservation": { + "header": "{{itemName}} için Rezervasyon", + "editItemDetails": "Rezervasyonu düzenle", + "lineItemId": "Satır öğesi ID", + "orderID": "Sipariş ID", + "description": "Açıklama", + "location": "Konum", + "inStockAtLocation": "Bu konumda mevcut", + "availableAtLocation": "Bu konumda mevcut", + "reservedAtLocation": "Bu konumda rezerve edildi", + "reservedAmount": "Rezerve edilecek miktar", + "create": "Rezervasyon Oluştur", + "itemToReserve": "Rezerve edilecek öğe", + "quantityPlaceholder": "Ne kadar rezerve etmek istiyorsunuz?", + "descriptionPlaceholder": "Bu rezervasyon türü nedir?", + "successToast": "Rezervasyon başarıyla oluşturuldu.", + "updateSuccessToast": "Rezervasyon başarıyla güncellendi.", + "deleteSuccessToast": "Rezervasyon başarıyla silindi.", + "errors": { + "noAvaliableQuantity": "Stok konumunda mevcut miktar yok.", + "quantityOutOfRange": "Minimum miktar 1 ve maksimum miktar {{max}}" + } + }, + "toast": { + "updateLocations": "Konumlar başarıyla güncellendi.", + "updateLevel": "Envanter seviyesi başarıyla güncellendi.", + "updateItem": "Envanter öğesi başarıyla güncellendi." + } + }, + "giftCards": { + "domain": "Hediye Kartları", + "editGiftCard": "Hediye Kartını Düzenle", + "createGiftCard": "Hediye Kartı Oluştur", + "createGiftCardHint": "Mağazanızda ödeme yöntemi olarak kullanılabilecek bir hediye kartı manuel olarak oluşturun.", + "selectRegionFirst": "Önce bir bölge seçin", + "deleteGiftCardWarning": "{{code}} hediye kartını silmek üzeresiniz. Bu işlem geri alınamaz.", + "balanceHigherThanValue": "Bakiye, orijinal miktardan yüksek olamaz.", + "balanceLowerThanZero": "Bakiye negatif olamaz.", + "expiryDateHint": "Ülkelerin hediye kartı son kullanma tarihleriyle ilgili farklı yasaları vardır. Son kullanma tarihini belirlemeden önce yerel düzenlemeleri kontrol ettiğinizden emin olun.", + "regionHint": "Hediye kartının bölgesini değiştirmek, para birimini de değiştirecek ve bu durum parasal değerini etkileyebilir.", + "enabledHint": "Hediye kartının etkin veya devre dışı olup olmadığını belirtin.", + "balance": "Bakiye", + "currentBalance": "Güncel bakiye", + "initialBalance": "Başlangıç bakiyesi", + "personalMessage": "Kişisel mesaj", + "recipient": "Alıcı" + }, + "customers": { + "domain": "Müşteriler", + "list": { + "noRecordsMessage": "Müşterileriniz burada görünecek." + }, + "create": { + "header": "Müşteri Oluştur", + "hint": "Yeni bir müşteri oluşturun ve detaylarını yönetin.", + "successToast": "{{email}} müşterisi başarıyla oluşturuldu." + }, + "groups": { + "label": "Müşteri grupları", + "remove": "\"{{name}}\" müşteri grubundan müşteriyi kaldırmak istediğinizden emin misiniz?", + "removeMany": "Müşteriyi aşağıdaki müşteri gruplarından kaldırmak istediğinizden emin misiniz: {{groups}}?", + "alreadyAddedTooltip": "Müşteri zaten bu müşteri grubunda.", + "list": { + "noRecordsMessage": "Bu müşteri herhangi bir gruba ait değil." + }, + "add": { + "success": "Müşteri eklendi: {{groups}}.", + "list": { + "noRecordsMessage": "Lütfen önce bir müşteri grubu oluşturun." + } + }, + "removed": { + "success": "Müşteri çıkarıldı: {{groups}}.", + "list": { + "noRecordsMessage": "Lütfen önce bir müşteri grubu oluşturun." + } + } + }, + "edit": { + "header": "Müşteriyi Düzenle", + "emailDisabledTooltip": "Kayıtlı müşteriler için e-posta adresi değiştirilemez.", + "successToast": "{{email}} müşterisi başarıyla güncellendi." + }, + "delete": { + "title": "Müşteriyi Sil", + "description": "{{email}} müşterisini silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "{{email}} müşterisi başarıyla silindi." + }, + "fields": { + "guest": "Misafir", + "registered": "Kayıtlı", + "groups": "Gruplar" + }, + "registered": "Kayıtlı", + "guest": "Misafir", + "hasAccount": "Hesabı var" + }, + "customerGroups": { + "domain": "Müşteri Grupları", + "subtitle": "Müşterileri gruplara ayırın. Gruplar farklı promosyonlara ve fiyatlara sahip olabilir.", + "create": { + "header": "Müşteri Grubu Oluştur", + "hint": "Müşterilerinizi segmente etmek için yeni bir müşteri grubu oluşturun.", + "successToast": "{{name}} müşteri grubu başarıyla oluşturuldu." + }, + "edit": { + "header": "Müşteri Grubunu Düzenle", + "successToast": "{{name}} müşteri grubu başarıyla güncellendi." + }, + "delete": { + "title": "Müşteri Grubunu Sil", + "description": "{{name}} müşteri grubunu silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "{{name}} müşteri grubu başarıyla silindi." + }, + "customers": { + "alreadyAddedTooltip": "Müşteri zaten gruba eklenmiş.", + "add": { + "successToast_one": "Müşteri gruba başarıyla eklendi.", + "successToast_other": "Müşteriler gruba başarıyla eklendi.", + "list": { + "noRecordsMessage": "Önce bir müşteri oluşturun." + } + }, + "remove": { + "title_one": "Müşteriyi kaldır", + "title_other": "Müşterileri kaldır", + "description_one": "Müşteri grubundan {{count}} müşteriyi kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "description_other": "Müşteri grubundan {{count}} müşteriyi kaldırmak üzeresiniz. Bu işlem geri alınamaz." + }, + "list": { + "noRecordsMessage": "Bu grupta müşteri yok." + } + } + }, + "orders": { + "domain": "Siparişler", + "claim": "Talep", + "exchange": "Değişim", + "return": "İade", + "cancelWarning": "{{id}} numaralı siparişi iptal etmek üzeresiniz. Bu işlem geri alınamaz.", + "onDateFromSalesChannel": "{{date}} tarihinde {{salesChannel}} üzerinden", + "list": { + "noRecordsMessage": "Siparişleriniz burada görünecektir." + }, + "summary": { + "requestReturn": "İade talebi", + "allocateItems": "Ürünleri tahsis et", + "editOrder": "Siparişi düzenle", + "editOrderContinue": "Sipariş düzenlemeye devam et", + "inventoryKit": "{{count}}x envanter öğesinden oluşur", + "itemTotal": "Ürün Toplamı", + "shippingTotal": "Kargo Toplamı", + "discountTotal": "İndirim Toplamı", + "taxTotalIncl": "Vergi Toplamı (dahil)", + "itemSubtotal": "Ürün Ara Toplamı", + "shippingSubtotal": "Kargo Ara Toplamı", + "discountSubtotal": "İndirim Ara Toplamı", + "taxTotal": "Vergi Toplamı" + }, + "payment": { + "title": "Ödemeler", + "isReadyToBeCaptured": "Ödeme <0/> alınmaya hazır.", + "totalPaidByCustomer": "Müşteri tarafından ödenen toplam", + "capture": "Ödemeyi al", + "capture_short": "Al", + "refund": "İade", + "markAsPaid": "Ödendi olarak işaretle", + "statusLabel": "Ödeme durumu", + "statusTitle": "Ödeme Durumu", + "status": { + "notPaid": "Ödenmedi", + "authorized": "Yetkilendirildi", + "partiallyAuthorized": "Kısmen yetkilendirildi", + "awaiting": "Bekleniyor", + "captured": "Alındı", + "partiallyRefunded": "Kısmen iade edildi", + "partiallyCaptured": "Kısmen alındı", + "refunded": "İade edildi", + "canceled": "İptal edildi", + "requiresAction": "Eylem gerekiyor" + }, + "capturePayment": "{{amount}} tutarında ödeme alınacak.", + "capturePaymentSuccess": "{{amount}} tutarındaki ödeme başarıyla alındı", + "markAsPaidPayment": "{{amount}} tutarındaki ödeme ödenmiş olarak işaretlenecek.", + "markAsPaidPaymentSuccess": "{{amount}} tutarındaki ödeme başarıyla ödenmiş olarak işaretlendi", + "createRefund": "İade Oluştur", + "refundPaymentSuccess": "{{amount}} tutarındaki iade başarılı", + "createRefundWrongQuantity": "Miktar 1 ile {{number}} arasında bir sayı olmalıdır", + "refundAmount": "{{ amount }} tutarını iade et", + "paymentLink": "{{ amount }} için ödeme bağlantısını kopyala", + "selectPaymentToRefund": "İade edilecek ödemeyi seçin" + }, + "edits": { + "title": "Siparişi düzenle", + "confirm": "Düzenlemeyi Onayla", + "confirmText": "Bir Sipariş Düzenlemeyi onaylamak üzeresiniz. Bu işlem geri alınamaz.", + "cancel": "Düzenlemeyi İptal Et", + "currentItems": "Mevcut ürünler", + "currentItemsDescription": "Ürün miktarını ayarla veya çıkar.", + "addItemsDescription": "Siparişe yeni ürünler ekleyebilirsiniz.", + "addItems": "Ürünleri ekle", + "amountPaid": "Ödenen tutar", + "newTotal": "Yeni toplam", + "differenceDue": "Fark ödenecek", + "create": "Siparişi Düzenle", + "currentTotal": "Mevcut toplam", + "noteHint": "Düzenleme için dahili bir not ekleyin", + "cancelSuccessToast": "Sipariş düzenlemesi iptal edildi", + "createSuccessToast": "Sipariş düzenleme talebi oluşturuldu", + "activeChangeError": "Siparişte (iade, talep, değişim vb.) aktif bir değişiklik zaten var. Lütfen siparişi düzenlemeden önce bu değişikliği tamamlayın veya iptal edin.", + "panel": { + "title": "Sipariş düzenleme talep edildi", + "titlePending": "Sipariş düzenleme bekliyor" + }, + "toast": { + "canceledSuccessfully": "Sipariş düzenlemesi iptal edildi", + "confirmedSuccessfully": "Sipariş düzenlemesi onaylandı" + }, + "validation": { + "quantityLowerThanFulfillment": "Miktar, tamamlanan miktardan daha az veya eşit olamaz" + } + }, + "returns": { + "create": "İade Oluştur", + "confirm": "İadeyi Onayla", + "confirmText": "Bir iade işlemini onaylamak üzeresiniz. Bu işlem geri alınamaz.", + "inbound": "Gelen", + "outbound": "Giden", + "sendNotification": "Bildirim gönder", + "sendNotificationHint": "Müşteriyi iade hakkında bilgilendir.", + "returnTotal": "İade toplamı", + "inboundTotal": "Gelen toplam", + "refundAmount": "İade tutarı", + "outstandingAmount": "Kalan tutar", + "reason": "Sebep", + "reasonHint": "Müşterinin neden ürünleri iade etmek istediğini seçin.", + "note": "Not", + "noInventoryLevel": "Stok seviyesi yok", + "noInventoryLevelDesc": "Seçilen konum için seçilen ürünlerin stok seviyesi yoktur. İade talep edilebilir ancak seçilen konum için bir stok seviyesi oluşturulana kadar alınamaz.", + "noteHint": "Belirtmek istediğiniz bir şey varsa serbestçe yazabilirsiniz.", + "location": "Konum", + "locationHint": "Ürünleri hangi konuma iade etmek istediğinizi seçin.", + "inboundShipping": "İade teslimatı", + "inboundShippingHint": "Hangi yöntemi kullanmak istediğinizi seçin.", + "returnableQuantityLabel": "İade edilebilir miktar", + "refundableAmountLabel": "İade edilebilir tutar", + "returnRequestedInfo": "{{requestedItemsCount}}x ürün için iade talebi", + "returnReceivedInfo": "{{requestedItemsCount}}x ürün iade alındı", + "itemReceived": "Ürünler alındı", + "returnRequested": "İade talep edildi", + "damagedItemReceived": "Hasarlı ürünler alındı", + "damagedItemsReturned": "{{quantity}}x hasarlı ürün iade edildi", + "activeChangeError": "Bu siparişte aktif bir değişiklik işlemi devam ediyor. Lütfen değişikliği tamamlayın veya iptal edin.", + "cancel": { + "title": "İadeyi İptal Et", + "description": "İade talebini iptal etmek istediğinizden emin misiniz?" + }, + "placeholders": { + "noReturnShippingOptions": { + "title": "İade teslimat seçenekleri bulunamadı", + "hint": "Seçilen konum için bir iade teslimat seçeneği oluşturulmadı. Konum & teslimat bölümünde bir tane oluşturabilirsiniz." + }, + "outboundShippingOptions": { + "title": "Giden teslimat seçenekleri bulunamadı", + "hint": "Seçilen konum için bir giden teslimat seçeneği oluşturulmadı. Konum & teslimat bölümünde bir tane oluşturabilirsiniz." + } + }, + "receive": { + "action": "Ürünleri al", + "receiveItems": "{{ returnType }} {{ id }}", + "restockAll": "Tüm ürünleri stokla", + "itemsLabel": "Alınan ürünler", + "title": "#{{returnId}} için ürünleri al", + "sendNotificationHint": "Alınan iade hakkında müşteriyi bilgilendirin.", + "inventoryWarning": "Lütfen yukarıdaki girişlerinize göre stok seviyelerinin otomatik olarak ayarlanacağını unutmayın.", + "writeOffInputLabel": "Kaç ürün hasarlı?", + "toast": { + "success": "İade başarıyla alındı.", + "errorLargeValue": "Miktar, talep edilen ürün miktarından fazla.", + "errorNegativeValue": "Miktar negatif olamaz.", + "errorLargeDamagedValue": "Hasarlı ürün miktarı + hasarsız alınan ürün miktarı toplam ürün miktarını aşıyor. Lütfen hasarsız ürün miktarını azaltın." + } + }, + "toast": { + "canceledSuccessfully": "İade başarıyla iptal edildi", + "confirmedSuccessfully": "İade başarıyla onaylandı" + }, + "panel": { + "title": "İade başlatıldı", + "description": "Tamamlanması gereken açık bir iade talebi var" + } + }, + "claims": { + "create": "Talep Oluştur", + "confirm": "Talebi Onayla", + "confirmText": "Bir Talebi onaylamak üzeresiniz. Bu işlem geri alınamaz.", + "manage": "Talebi Yönet", + "outbound": "Giden", + "outboundItemAdded": "{{itemsCount}}x talep yoluyla eklendi", + "outboundTotal": "Giden toplam", + "outboundShipping": "Giden teslimat", + "outboundShippingHint": "Hangi yöntemi kullanmak istediğinizi seçin.", + "refundAmount": "Tahmini fark", + "activeChangeError": "Bu siparişte aktif bir değişiklik işlemi var. Lütfen önceki değişikliği tamamlayın veya iptal edin.", + "actions": { + "cancelClaim": { + "successToast": "Talep başarıyla iptal edildi." + } + }, + "cancel": { + "title": "Talebi İptal Et", + "description": "Talebi iptal etmek istediğinizden emin misiniz?" + }, + "tooltips": { + "onlyReturnShippingOptions": "Bu liste yalnızca iade teslimat seçeneklerinden oluşacaktır." + }, + "toast": { + "canceledSuccessfully": "Talep başarıyla iptal edildi", + "confirmedSuccessfully": "Talep başarıyla onaylandı" + }, + "panel": { + "title": "Talep başlatıldı", + "description": "Tamamlanması gereken açık bir talep talebi var" + } + }, + "exchanges": { + "create": "Değişim Oluştur", + "manage": "Değişimi Yönet", + "confirm": "Değişimi Onayla", + "confirmText": "Bir Değişimi onaylamak üzeresiniz. Bu işlem geri alınamaz.", + "outbound": "Giden", + "outboundItemAdded": "{{itemsCount}}x değişim yoluyla eklendi", + "outboundTotal": "Giden toplam", + "outboundShipping": "Giden teslimat", + "outboundShippingHint": "Hangi yöntemi kullanmak istediğinizi seçin.", + "refundAmount": "Tahmini fark", + "activeChangeError": "Bu siparişte aktif bir değişiklik işlemi var. Lütfen önceki değişikliği tamamlayın veya iptal edin.", + "actions": { + "cancelExchange": { + "successToast": "Değişim başarıyla iptal edildi." + } + }, + "cancel": { + "title": "Değişimi İptal Et", + "description": "Değişimi iptal etmek istediğinizden emin misiniz?" + }, + "tooltips": { + "onlyReturnShippingOptions": "Bu liste yalnızca iade teslimat seçeneklerinden oluşacaktır." + }, + "toast": { + "canceledSuccessfully": "Değişim başarıyla iptal edildi", + "confirmedSuccessfully": "Değişim başarıyla onaylandı" + }, + "panel": { + "title": "Değişim başlatıldı", + "description": "Tamamlanması gereken açık bir değişim talebi var" + } + }, + "reservations": { + "allocatedLabel": "Tahsis Edildi", + "notAllocatedLabel": "Tahsis Edilmedi" + }, + "allocateItems": { + "action": "Ürünleri tahsis et", + "title": "Sipariş ürünlerini tahsis et", + "locationDescription": "Hangi konumdan tahsis etmek istediğinizi seçin.", + "itemsToAllocate": "Tahsis edilecek ürünler", + "itemsToAllocateDesc": "Tahsis etmek istediğiniz ürün sayısını seçin", + "search": "Ürünleri ara", + "consistsOf": "{{num}}x envanter öğesinden oluşur", + "requires": "Her bir varyant için {{num}} gerektirir", + "toast": { + "created": "Ürünler başarıyla tahsis edildi" + }, + "error": { + "quantityNotAllocated": "Tahsis edilmemiş ürünler var." + } + }, + "shipment": { + "title": "Gönderimi İşaretle", + "trackingNumber": "Takip numarası", + "addTracking": "Takip numarası ekle", + "sendNotification": "Bildirim gönder", + "sendNotificationHint": "Müşteriye bu gönderim hakkında bilgi ver.", + "toastCreated": "Gönderim başarıyla oluşturuldu." + }, + "fulfillment": { + "cancelWarning": "Bir sipariş tamamlamayı iptal etmek üzeresiniz. Bu işlem geri alınamaz.", + "markAsDeliveredWarning": "Tamamlamayı teslim edilmiş olarak işaretlemek üzeresiniz. Bu işlem geri alınamaz.", + "unfulfilledItems": "Tamamlanmamış Ürünler", + "statusLabel": "Tamamlama durumu", + "statusTitle": "Tamamlama Durumu", + "fulfillItems": "Ürünleri tamamla", + "awaitingFulfillmentBadge": "Tamamlanmayı bekliyor", + "requiresShipping": "Teslimat gerekli", + "number": "Tamamlama #{{number}}", + "itemsToFulfill": "Tamamlanacak ürünler", + "create": "Tamamlama Oluştur", + "available": "Mevcut", + "inStock": "Stokta", + "markAsShipped": "Gönderildi olarak işaretle", + "markAsDelivered": "Teslim edildi olarak işaretle", + "itemsToFulfillDesc": "Tamamlanacak ürünleri ve miktarları seçin", + "locationDescription": "Ürünleri hangi konumdan tamamlamak istediğinizi seçin.", + "sendNotificationHint": "Oluşturulan tamamlama hakkında müşterilere bilgi verin.", + "methodDescription": "Müşterinin seçtiği teslimat yönteminden farklı bir yöntem seçin", + "error": { + "wrongQuantity": "Tamamlanacak sadece bir ürün mevcut", + "wrongQuantity_other": "Miktar 1 ile {{number}} arasında olmalıdır", + "noItems": "Tamamlanacak ürün yok." + }, + "status": { + "notFulfilled": "Tamamlanmadı", + "partiallyFulfilled": "Kısmen tamamlandı", + "fulfilled": "Tamamlandı", + "partiallyShipped": "Kısmen gönderildi", + "shipped": "Gönderildi", + "delivered": "Teslim edildi", + "partiallyDelivered": "Kısmen teslim edildi", + "partiallyReturned": "Kısmen iade edildi", + "returned": "İade edildi", + "canceled": "İptal edildi", + "requiresAction": "Eylem gerekli" + }, + "toast": { + "created": "Tamamlama başarıyla oluşturuldu", + "canceled": "Tamamlama başarıyla iptal edildi", + "fulfillmentShipped": "Gönderilmiş bir tamamlamayı iptal edemezsiniz", + "fulfillmentDelivered": "Tamamlama başarıyla teslim edildi olarak işaretlendi" + }, + "trackingLabel": "Takip", + "shippingFromLabel": "Gönderen", + "itemsLabel": "Ürünler" + }, + "refund": { + "title": "İade Oluştur", + "sendNotificationHint": "Oluşturulan iade hakkında müşterilere bilgi verin.", + "systemPayment": "Sistem ödemesi", + "systemPaymentDesc": "Ödemelerinizden biri bir sistem ödemesidir. Bu tür ödemeler için işlemler Medusa tarafından yönetilmez.", + "error": { + "amountToLarge": "Orijinal sipariş tutarından daha fazlasını iade edemezsiniz.", + "amountNegative": "İade tutarı pozitif bir sayı olmalıdır.", + "reasonRequired": "Lütfen bir iade sebebi seçin." + } + }, + "customer": { + "contactLabel": "İletişim", + "editEmail": "E-posta düzenle", + "transferOwnership": "Sahipliği devret", + "editBillingAddress": "Fatura adresini düzenle", + "editShippingAddress": "Teslimat adresini düzenle" + }, + "activity": { + "header": "Aktivite", + "showMoreActivities_one": "{{count}} aktiviteyi daha göster", + "showMoreActivities_other": "{{count}} aktiviteyi daha göster", + "comment": { + "label": "Yorum", + "placeholder": "Bir yorum bırak", + "addButtonText": "Yorum ekle", + "deleteButtonText": "Yorumu sil" + }, + "events": { + "common": { + "toReturn": "İade edilecek", + "toSend": "Gönderilecek" + }, + "placed": { + "title": "Sipariş verildi", + "fromSalesChannel": "{{salesChannel}} üzerinden" + }, + "canceled": { + "title": "Sipariş iptal edildi" + }, + "payment": { + "awaiting": "Ödeme bekleniyor", + "captured": "Ödeme alındı", + "canceled": "Ödeme iptal edildi", + "refunded": "Ödeme iade edildi" + }, + "fulfillment": { + "created": "Ürünler tamamlandı", + "canceled": "Tamamlama iptal edildi", + "shipped": "Ürünler gönderildi", + "delivered": "Ürünler teslim edildi", + "items_one": "{{count}} ürün", + "items_other": "{{count}} ürün" + }, + "return": { + "created": "İade #{{returnId}} talep edildi", + "canceled": "İade #{{returnId}} iptal edildi", + "received": "İade #{{returnId}} alındı", + "items_one": "{{count}} ürün iade edildi", + "items_other": "{{count}} ürün iade edildi" + }, + "note": { + "comment": "Yorum", + "byLine": "{{author}} tarafından" + }, + "claim": { + "created": "Talep #{{claimId}} talep edildi", + "canceled": "Talep #{{claimId}} iptal edildi", + "itemsInbound": "{{count}} ürün iade edilecek", + "itemsOutbound": "{{count}} ürün gönderilecek" + }, + "exchange": { + "created": "Değişim #{{exchangeId}} talep edildi", + "canceled": "Değişim #{{exchangeId}} iptal edildi", + "itemsInbound": "{{count}} ürün iade edilecek", + "itemsOutbound": "{{count}} ürün gönderilecek" + }, + "edit": { + "requested": "Sipariş düzenlemesi #{{editId}} talep edildi", + "confirmed": "Sipariş düzenlemesi #{{editId}} onaylandı" + } + } + }, + "fields": { + "displayId": "Görüntüleme Kimliği", + "refundableAmount": "İade edilebilir tutar", + "returnableQuantity": "İade edilebilir miktar" + } + }, + "draftOrders": { + "domain": "Taslak Siparişler", + "deleteWarning": "Taslak sipariş {{id}} silinmek üzere. Bu işlem geri alınamaz.", + "paymentLinkLabel": "Ödeme bağlantısı", + "cartIdLabel": "Sepet ID", + "markAsPaid": { + "label": "Ödenmiş olarak işaretle", + "warningTitle": "Ödenmiş Olarak İşaretle", + "warningDescription": "Taslak siparişi ödenmiş olarak işaretlemek üzeresiniz. Bu işlem geri alınamaz ve daha sonra ödeme toplamak mümkün olmayacaktır." + }, + "status": { + "open": "Açık", + "completed": "Tamamlandı" + }, + "create": { + "createDraftOrder": "Taslak Sipariş Oluştur", + "createDraftOrderHint": "Bir sipariş yerleştirilmeden önce detaylarını yönetmek için yeni bir taslak sipariş oluşturun.", + "chooseRegionHint": "Bölgeyi seçin", + "existingItemsLabel": "Mevcut ürünler", + "existingItemsHint": "Taslak siparişe mevcut ürünleri ekleyin.", + "customItemsLabel": "Özel ürünler", + "customItemsHint": "Taslak siparişe özel ürünler ekleyin.", + "addExistingItemsAction": "Mevcut ürünleri ekle", + "addCustomItemAction": "Özel ürün ekle", + "noCustomItemsAddedLabel": "Henüz özel ürün eklenmedi", + "noExistingItemsAddedLabel": "Henüz mevcut ürün eklenmedi", + "chooseRegionTooltip": "Önce bir bölge seçin", + "useExistingCustomerLabel": "Mevcut müşteriyi kullan", + "addShippingMethodsAction": "teslimat yöntemlerini ekle", + "unitPriceOverrideLabel": "Birim fiyat geçersiz kılma", + "shippingOptionLabel": "teslimat seçeneği", + "shippingOptionHint": "Taslak sipariş için teslimat seçeneğini seçin.", + "shippingPriceOverrideLabel": "teslimat fiyatı geçersiz kılma", + "shippingPriceOverrideHint": "Taslak sipariş için teslimat fiyatını geçersiz kılın.", + "sendNotificationLabel": "Bildirim gönder", + "sendNotificationHint": "Taslak sipariş oluşturulduğunda müşteriye bir bildirim gönderin." + }, + "validation": { + "requiredEmailOrCustomer": "E-posta veya müşteri gereklidir.", + "requiredItems": "En az bir ürün gereklidir.", + "invalidEmail": "E-posta geçerli bir e-posta adresi olmalıdır." + } + }, + "stockLocations": { + "domain": "Konumlar ve teslimat", + "list": { + "description": "Mağazanızın stok konumlarını ve teslimat seçeneklerini yönetin." + }, + "create": { + "header": "Stok Konumu Oluştur", + "hint": "Bir stok konumu, ürünlerin depolandığı ve gönderildiği fiziksel bir yerdir.", + "successToast": "Konum {{name}} başarıyla oluşturuldu." + }, + "edit": { + "header": "Stok Konumunu Düzenle", + "viewInventory": "Envanteri görüntüle", + "successToast": "Konum {{name}} başarıyla güncellendi." + }, + "delete": { + "confirmation": "Stok konumu {{name}} silinmek üzere. Bu işlem geri alınamaz." + }, + "fulfillmentProviders": { + "header": "Tamamlama Sağlayıcıları", + "shippingOptionsTooltip": "Bu açılır liste yalnızca bu konum için etkinleştirilen sağlayıcıları içerecektir. Açılır liste devre dışıysa konuma ekleyin.", + "label": "Bağlı tamamlama sağlayıcıları", + "connectedTo": "{{total}} sağlayıcıdan {{count}}'ine bağlı", + "noProviders": "Bu Stok Konumu hiçbir tamamlama sağlayıcısına bağlı değil.", + "action": "Sağlayıcıları Bağla", + "successToast": "Stok konumu için tamamlama sağlayıcıları başarıyla güncellendi." + }, + "fulfillmentSets": { + "pickup": { + "header": "Alma" + }, + "shipping": { + "header": "Teslimat" + }, + "disable": { + "confirmation": "{{name}} devre dışı bırakmak istediğinizden emin misiniz? Bu, ilgili tüm hizmet bölgelerini ve teslimat seçeneklerini silecektir ve geri alınamaz.", + "pickup": "Alma başarıyla devre dışı bırakıldı.", + "shipping": "Teslimat başarıyla devre dışı bırakıldı." + }, + "enable": { + "pickup": "Alma başarıyla etkinleştirildi.", + "shipping": "Teslimat başarıyla etkinleştirildi." + } + }, + "sidebar": { + "header": "Teslimat Yapılandırması", + "shippingProfiles": { + "label": "Teslimat Profilleri", + "description": "Ürünleri teslimat gereksinimlerine göre gruplayın" + } + }, + "salesChannels": { + "header": "Satış Kanalları", + "label": "Bağlı satış kanalları", + "connectedTo": "{{total}} satış kanalından {{count}}'ine bağlı", + "noChannels": "Konum hiçbir satış kanalına bağlı değil.", + "action": "Satış kanallarını bağla", + "successToast": "Satış kanalları başarıyla güncellendi." + }, + "shippingOptions": { + "create": { + "shipping": { + "header": "{{zone}} için Teslimat Seçeneği Oluştur", + "hint": "Bu konumdan ürünlerin nasıl gönderileceğini tanımlamak için yeni bir teslimat seçeneği oluşturun.", + "label": "Teslimat seçenekleri", + "successToast": "Teslimat seçeneği {{name}} başarıyla oluşturuldu." + }, + "returns": { + "header": "{{zone}} için İade Seçeneği Oluştur", + "hint": "Bu konuma ürünlerin nasıl iade edileceğini tanımlamak için yeni bir iade seçeneği oluşturun.", + "label": "İade seçenekleri", + "successToast": "İade seçeneği {{name}} başarıyla oluşturuldu." + }, + "tabs": { + "details": "Detaylar", + "prices": "Fiyatlar" + }, + "action": "Seçenek oluştur" + }, + "delete": { + "confirmation": "teslimat seçeneği {{name}} silinmek üzere. Bu işlem geri alınamaz.", + "successToast": "teslimat seçeneği {{name}} başarıyla silindi." + }, + "edit": { + "header": "teslimat Seçeneğini Düzenle", + "action": "Seçeneği düzenle", + "successToast": "teslimat seçeneği {{name}} başarıyla güncellendi." + }, + "pricing": { + "action": "Fiyatları düzenle" + }, + "fields": { + "count": { + "shipping_one": "{{count}} teslimat seçeneği", + "shipping_other": "{{count}} teslimat seçeneği", + "returns_one": "{{count}} iade seçeneği", + "returns_other": "{{count}} iade seçeneği" + }, + "priceType": { + "label": "Fiyat türü", + "options": { + "fixed": { + "label": "Sabit", + "hint": "teslimat seçeneğinin fiyatı sabittir ve siparişin içeriğine göre değişmez." + }, + "calculated": { + "label": "Hesaplanmış", + "hint": "teslimat seçeneğinin fiyatı, ödeme sırasında tamamlama sağlayıcısı tarafından hesaplanır." + } + } + }, + "enableInStore": { + "label": "Mağazada etkinleştir", + "hint": "Müşterilerin bu seçeneği ödeme sırasında kullanıp kullanamayacağı." + }, + "provider": "Tamamlama sağlayıcısı", + "profile": "teslimat profili" + } + }, + "serviceZones": { + "create": { + "headerPickup": "{{location}} konumundan Alım için Hizmet Bölgesi Oluştur", + "headerShipping": "{{location}} konumundan teslimat için Hizmet Bölgesi Oluştur", + "action": "Hizmet bölgesi oluştur", + "successToast": "Hizmet bölgesi {{name}} başarıyla oluşturuldu." + }, + "edit": { + "header": "Hizmet Bölgesini Düzenle", + "successToast": "Hizmet bölgesi {{name}} başarıyla güncellendi." + }, + "delete": { + "confirmation": "Hizmet bölgesi {{name}} silinmek üzere. Bu işlem geri alınamaz.", + "successToast": "Hizmet bölgesi {{name}} başarıyla silindi." + }, + "manageAreas": { + "header": "{{name}} için Alanları Yönet", + "action": "Alanları yönet", + "label": "Alanlar", + "hint": "Hizmet bölgesinin kapsadığı coğrafi alanları seçin.", + "successToast": "{{name}} için alanlar başarıyla güncellendi." + }, + "fields": { + "noRecords": "Ekleyebileceğiniz teslimat seçeneklerine sahip hizmet bölgeleri yok.", + "tip": "Bir hizmet bölgesi, coğrafi bölgeler veya alanlardan oluşur. Bu, belirli konumlara sunulabilir teslimat seçeneklerini sınırlamak için kullanılır." + } + } + }, + "shippingProfile": { + "domain": "teslimat Profilleri", + "subtitle": "Benzer teslimat gereksinimlerine sahip ürünleri profillere gruplandırın.", + "create": { + "header": "teslimat Profili Oluştur", + "hint": "Benzer teslimat gereksinimlerine sahip ürünleri gruplandırmak için yeni bir teslimat profili oluşturun.", + "successToast": "teslimat profili {{name}} başarıyla oluşturuldu." + }, + "delete": { + "title": "teslimat Profilini Sil", + "description": "teslimat profili {{name}} silinmek üzere. Bu işlem geri alınamaz.", + "successToast": "teslimat profili {{name}} başarıyla silindi." + }, + "tooltip": { + "type": "teslimat profili türünü girin, örneğin: Ağır, Aşırı Büyük, Sadece teslimat, vb." + } + }, + "taxRegions": { + "domain": "Vergi Bölgeleri", + "list": { + "hint": "Müşterilerinizin farklı ülkeler ve bölgelerden alışveriş yaparken ödediği tutarı yönetin." + }, + "delete": { + "confirmation": "Bir vergi bölgesini silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "Vergi bölgesi başarıyla silindi." + }, + "create": { + "header": "Vergi Bölgesi Oluştur", + "hint": "Belirli bir ülke için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun.", + "errors": { + "rateIsRequired": "Varsayılan bir vergi oranı oluştururken vergi oranı gereklidir.", + "nameIsRequired": "Varsayılan bir vergi oranı oluştururken isim gereklidir." + }, + "successToast": "Vergi bölgesi başarıyla oluşturuldu." + }, + "province": { + "header": "İller", + "create": { + "header": "İl Vergi Bölgesi Oluştur", + "hint": "Belirli bir il için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "state": { + "header": "Eyaletler", + "create": { + "header": "Eyalet Vergi Bölgesi Oluştur", + "hint": "Belirli bir eyalet için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "stateOrTerritory": { + "header": "Eyaletler veya Bölgeler", + "create": { + "header": "Eyalet/Bölge Vergi Bölgesi Oluştur", + "hint": "Belirli bir eyalet/bölge için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "county": { + "header": "İlçeler", + "create": { + "header": "İlçe Vergi Bölgesi Oluştur", + "hint": "Belirli bir ilçe için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "region": { + "header": "Bölgeler", + "create": { + "header": "Bölge Vergi Bölgesi Oluştur", + "hint": "Belirli bir bölge için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "department": { + "header": "Departmanlar", + "create": { + "header": "Departman Vergi Bölgesi Oluştur", + "hint": "Belirli bir departman için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "territory": { + "header": "Bölgeler", + "create": { + "header": "Bölge Vergi Bölgesi Oluştur", + "hint": "Belirli bir bölge için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "prefecture": { + "header": "Prefektörlükler", + "create": { + "header": "Prefektörlük Vergi Bölgesi Oluştur", + "hint": "Belirli bir prefektörlük için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "district": { + "header": "Bölgeler", + "create": { + "header": "Bölge Vergi Bölgesi Oluştur", + "hint": "Belirli bir bölge için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "governorate": { + "header": "Valilikler", + "create": { + "header": "Valilik Vergi Bölgesi Oluştur", + "hint": "Belirli bir valilik için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "canton": { + "header": "Kantonlar", + "create": { + "header": "Kanton Vergi Bölgesi Oluştur", + "hint": "Belirli bir kanton için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "emirate": { + "header": "Emirlikler", + "create": { + "header": "Emirlik Vergi Bölgesi Oluştur", + "hint": "Belirli bir emirlik için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "sublevel": { + "header": "Alt Seviyeler", + "create": { + "header": "Alt Seviye Vergi Bölgesi Oluştur", + "hint": "Belirli bir alt seviye için vergi oranlarını tanımlamak üzere yeni bir vergi bölgesi oluşturun." + } + }, + "taxOverrides": { + "header": "Aşım Oranları", + "create": { + "header": "Aşım Oranı Oluştur", + "hint": "Belirli koşullar için varsayılan vergi oranlarını aşan bir vergi oranı oluşturun." + }, + "edit": { + "header": "Aşım Oranını Düzenle", + "hint": "Belirli koşullar için varsayılan vergi oranlarını aşan bir vergi oranını düzenleyin." + } + }, + "taxRates": { + "create": { + "header": "Vergi Oranı Oluştur", + "hint": "Bir bölge için vergi oranını tanımlamak üzere yeni bir vergi oranı oluşturun.", + "successToast": "Vergi oranı başarıyla oluşturuldu." + }, + "edit": { + "header": "Vergi Oranını Düzenle", + "hint": "Bir bölge için vergi oranını tanımlamak üzere vergi oranını düzenleyin.", + "successToast": "Vergi oranı başarıyla güncellendi." + }, + "delete": { + "confirmation": "Vergi oranı {{name}} silinmek üzere. Bu işlem geri alınamaz.", + "successToast": "Vergi oranı başarıyla silindi." + } + }, + "fields": { + "isCombinable": { + "label": "Birleştirilebilir", + "hint": "Bu vergi oranının vergi bölgesindeki varsayılan oranla birleştirilebilir olup olmadığını belirtir.", + "true": "Birleştirilebilir", + "false": "Birleştirilemez" + }, + "defaultTaxRate": { + "label": "Varsayılan vergi oranı", + "tooltip": "Bu bölge için varsayılan vergi oranı. Örnek olarak, bir ülke veya bölge için standart KDV oranı.", + "action": "Varsayılan vergi oranı oluştur" + }, + "taxRate": "Vergi oranı", + "taxCode": "Vergi kodu", + "targets": { + "label": "Hedefler", + "hint": "Bu vergi oranının uygulanacağı hedefleri seçin.", + "options": { + "product": "Ürünler", + "productCollection": "Ürün koleksiyonları", + "productTag": "Ürün etiketleri", + "productType": "Ürün türleri", + "customerGroup": "Müşteri grupları" + }, + "operators": { + "in": "içinde", + "on": "üzerinde", + "and": "ve" + }, + "placeholders": { + "product": "Ürünleri ara", + "productCollection": "Ürün koleksiyonlarını ara", + "productTag": "Ürün etiketlerini ara", + "productType": "Ürün türlerini ara", + "customerGroup": "Müşteri gruplarını ara" + }, + "tags": { + "product": "Ürün", + "productCollection": "Ürün koleksiyonu", + "productTag": "Ürün etiketi", + "productType": "Ürün türü", + "customerGroup": "Müşteri grubu" + }, + "modal": { + "header": "Hedef ekle" + }, + "values_one": "{{count}} değer", + "values_other": "{{count}} değerler", + "numberOfTargets_one": "{{count}} hedef", + "numberOfTargets_other": "{{count}} hedefler", + "additionalValues_one": "ve {{count}} daha değer", + "additionalValues_other": "ve {{count}} daha değerler", + "action": "Hedef ekle" + }, + "sublevels": { + "labels": { + "province": "İl", + "state": "Eyalet", + "region": "Bölge", + "stateOrTerritory": "Eyalet/Bölge", + "department": "Departman", + "county": "İlçe", + "territory": "Bölge", + "prefecture": "Prefektörlük", + "district": "Bölge", + "governorate": "Valilik", + "emirate": "Emirlik", + "canton": "Kanton", + "sublevel": "Alt seviye kodu" + }, + "placeholders": { + "province": "İl seçin", + "state": "Eyalet seçin", + "region": "Bölge seçin", + "stateOrTerritory": "Eyalet/bölge seçin", + "department": "Departman seçin", + "county": "İlçe seçin", + "territory": "Bölge seçin", + "prefecture": "Prefektörlük seçin", + "district": "Bölge seçin", + "governorate": "Valilik seçin", + "emirate": "Emirlik seçin", + "canton": "Kanton seçin" + }, + "tooltips": { + "sublevel": "Alt seviye vergi bölgesi için ISO 3166-2 kodunu girin.", + "notPartOfCountry": "{{province}}, {{country}}'in bir parçası görünmüyor. Lütfen bu bilgiyi kontrol edin." + }, + "alert": { + "header": "Bu vergi bölgesi için alt seviyeler devre dışı", + "description": "Alt seviyeler, bu bölge için varsayılan olarak devre dışıdır. Eyaletler, iller veya bölgeler gibi alt seviyeleri oluşturmak için bunları etkinleştirebilirsiniz.", + "action": "Alt seviyeleri etkinleştir" + } + }, + "noDefaultRate": { + "label": "Varsayılan oran yok", + "tooltip": "Bu vergi bölgesinde varsayılan bir vergi oranı bulunmuyor. Örneğin, bir ülkenin standart KDV'si gibi bir oran varsa, lütfen bu bölgeye ekleyin." + } + } + }, + "promotions": { + "domain": "Promosyonlar", + "sections": { + "details": "Promosyon Detayları" + }, + "tabs": { + "template": "Tür", + "details": "Detaylar", + "campaign": "Kampanya" + }, + "fields": { + "type": "Tür", + "value_type": "Değer Türü", + "value": "Değer", + "campaign": "Kampanya", + "method": "Yöntem", + "allocation": "Dağıtım", + "addCondition": "Şart ekle", + "clearAll": "Tümünü Temizle", + "amount": { + "tooltip": "Miktarı ayarlamak için para birimi kodunu seçin" + }, + "conditions": { + "rules": { + "title": "Bu kodu kim kullanabilir?", + "description": "Hangi müşterinin promosyon kodunu kullanmasına izin verilir? Eğer değiştirilmezse, promosyon kodu tüm müşteriler tarafından kullanılabilir." + }, + "target-rules": { + "title": "Promosyon hangi ürünlere uygulanacak?", + "description": "Promosyon, aşağıdaki koşulları karşılayan ürünlere uygulanacaktır." + }, + "buy-rules": { + "title": "Promosyonu açmak için sepette ne olmalı?", + "description": "Bu koşullar sağlandığında, hedef ürünlerde promosyon etkinleştirilir." + } + } + }, + "tooltips": { + "campaignType": "Bir harcama bütçesi belirlemek için promosyonda para birimi kodu seçilmelidir." + }, + "errors": { + "requiredField": "Zorunlu alan", + "promotionTabError": "Devam etmeden önce Promosyon Sekmesindeki hataları düzeltin" + }, + "toasts": { + "promotionCreateSuccess": "Promosyon ({{code}}) başarıyla oluşturuldu." + }, + "create": {}, + "edit": { + "title": "Promosyon Detaylarını Düzenle", + "rules": { + "title": "Kullanım koşullarını düzenle" + }, + "target-rules": { + "title": "Ürün koşullarını düzenle" + }, + "buy-rules": { + "title": "Satın alma kurallarını düzenle" + } + }, + "campaign": { + "header": "Kampanya", + "edit": { + "header": "Kampanyayı Düzenle", + "successToast": "Promosyonun kampanyası başarıyla güncellendi." + }, + "actions": { + "goToCampaign": "Kampanyaya git" + } + }, + "campaign_currency": { + "tooltip": "Bu promosyonun para birimi. Detaylar sekmesinden değiştirin." + }, + "form": { + "required": "Zorunlu", + "and": "VE", + "selectAttribute": "Özellik Seçin", + "campaign": { + "existing": { + "title": "Mevcut Kampanya", + "description": "Promosyonu mevcut bir kampanyaya ekle.", + "placeholder": { + "title": "Mevcut kampanya yok", + "desc": "Birden fazla promosyonu izlemek ve bütçe sınırları belirlemek için bir tane oluşturabilirsiniz." + } + }, + "new": { + "title": "Yeni Kampanya", + "description": "Bu promosyon için yeni bir kampanya oluştur." + }, + "none": { + "title": "Kampanyasız", + "description": "Promosyonu kampanyaya bağlamadan devam et" + } + }, + "status": { + "title": "Durum" + }, + "method": { + "label": "Yöntem", + "code": { + "title": "Promosyon Kodu", + "description": "Müşteriler bu kodu ödeme sırasında girmelidir" + }, + "automatic": { + "title": "Otomatik", + "description": "Müşteriler bu promosyonu ödeme sırasında görecektir" + } + }, + "max_quantity": { + "title": "Maksimum Miktar", + "description": "Bu promosyonun uygulanacağı maksimum ürün miktarı." + }, + "type": { + "standard": { + "title": "Standart", + "description": "Standart bir promosyon" + }, + "buyget": { + "title": "Satın Al Kazan", + "description": "X Al Y Kazan promosyonu" + } + }, + "allocation": { + "each": { + "title": "Her biri", + "description": "Değeri her bir ürüne uygular" + }, + "across": { + "title": "Genel", + "description": "Değeri ürünler arasında uygular" + } + }, + "code": { + "title": "Kod", + "description": "Müşterilerinizin ödeme sırasında gireceği kod." + }, + "value": { + "title": "Promosyon Değeri" + }, + "value_type": { + "fixed": { + "title": "Promosyon Değeri", + "description": "İndirim miktarı. Örn: 100" + }, + "percentage": { + "title": "Promosyon Değeri", + "description": "İndirim yüzdesi. Örn: %8" + } + } + }, + "deleteWarning": "Promosyon {{code}}'u silmek üzeresiniz. Bu işlem geri alınamaz.", + "createPromotionTitle": "Promosyon Oluştur", + "type": "Promosyon türü", + "conditions": { + "add": "Şart ekle", + "list": { + "noRecordsMessage": "Promosyonun uygulanacağı ürünleri kısıtlamak için bir şart ekleyin." + } + } + }, + "campaigns": { + "domain": "Kampanyalar", + "details": "Kampanya detayları", + "status": { + "active": "Aktif", + "expired": "Süresi dolmuş", + "scheduled": "Planlanmış" + }, + "delete": { + "title": "Emin misiniz?", + "description": "'{{name}}' kampanyasını silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "'{{name}}' kampanyası başarıyla oluşturuldu." + }, + "edit": { + "header": "Kampanyayı Düzenle", + "description": "Kampanyanın detaylarını düzenleyin.", + "successToast": "'{{name}}' kampanyası başarıyla güncellendi." + }, + "configuration": { + "header": "Yapılandırma", + "edit": { + "header": "Kampanya Yapılandırmasını Düzenle", + "description": "Kampanyanın yapılandırmasını düzenleyin.", + "successToast": "Kampanya yapılandırması başarıyla güncellendi." + } + }, + "create": { + "title": "Kampanya Oluştur", + "description": "Bir promosyon kampanyası oluşturun.", + "hint": "Bir promosyon kampanyası oluşturun.", + "header": "Kampanya Oluştur", + "successToast": "'{{name}}' kampanyası başarıyla oluşturuldu." + }, + "fields": { + "name": "Ad", + "identifier": "Tanımlayıcı", + "start_date": "Başlangıç tarihi", + "end_date": "Bitiş tarihi", + "total_spend": "Harcanan bütçe", + "total_used": "Kullanılan bütçe", + "budget_limit": "Bütçe limiti", + "campaign_id": { + "hint": "Sadece promosyon ile aynı para birimine sahip kampanyalar bu listede gösterilir." + } + }, + "budget": { + "create": { + "hint": "Kampanya için bir bütçe oluşturun.", + "header": "Kampanya Bütçesi" + }, + "details": "Kampanya bütçesi", + "fields": { + "type": "Tür", + "currency": "Para birimi", + "limit": "Limit", + "used": "Kullanıldı" + }, + "type": { + "spend": { + "title": "Harcamalar", + "description": "Tüm promosyon kullanımlarının toplam indirimli miktarına bir sınır koyun." + }, + "usage": { + "title": "Kullanım", + "description": "Promosyonun kaç kez kullanılabileceğine bir sınır koyun." + } + }, + "edit": { + "header": "Kampanya Bütçesini Düzenle" + } + }, + "promotions": { + "remove": { + "title": "Promosyonu kampanyadan kaldır", + "description": "Kampanyadan {{count}} promosyon(ları) kaldırmak üzeresiniz. Bu işlem geri alınamaz." + }, + "alreadyAdded": "Bu promosyon zaten kampanyaya eklenmiştir.", + "alreadyAddedDiffCampaign": "Bu promosyon zaten farklı bir kampanyaya eklenmiş ({{name}}).", + "currencyMismatch": "Promosyon ve kampanyanın para birimi uyuşmuyor", + "toast": { + "success": "{{count}} promosyon başarıyla kampanyaya eklendi" + }, + "add": { + "list": { + "noRecordsMessage": "Önce bir promosyon oluşturun." + } + }, + "list": { + "noRecordsMessage": "Kampanyada promosyon bulunmamaktadır." + } + }, + "deleteCampaignWarning": "{{name}} kampanyasını silmek üzeresiniz. Bu işlem geri alınamaz.", + "totalSpend": "<0>{{amount}} <1>{{currency}}" + }, + "priceLists": { + "domain": "Fiyat Listeleri", + "subtitle": "Belirli koşullar için satış veya fiyat geçersiz kılmalar oluşturun.", + "delete": { + "confirmation": "{{title}} fiyat listesini silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "{{title}} fiyat listesi başarıyla silindi." + }, + "create": { + "header": "Fiyat Listesi Oluştur", + "subheader": "Ürünlerinizin fiyatlarını yönetmek için yeni bir fiyat listesi oluşturun.", + "tabs": { + "details": "Detaylar", + "products": "Ürünler", + "prices": "Fiyatlar" + }, + "successToast": "{{title}} fiyat listesi başarıyla oluşturuldu.", + "products": { + "list": { + "noRecordsMessage": "Önce bir ürün oluşturun." + } + } + }, + "edit": { + "header": "Fiyat Listesini Düzenle", + "successToast": "{{title}} fiyat listesi başarıyla güncellendi." + }, + "configuration": { + "header": "Yapılandırma", + "edit": { + "header": "Fiyat Listesi Yapılandırmasını Düzenle", + "description": "Fiyat listesinin yapılandırmasını düzenleyin.", + "successToast": "Fiyat listesi yapılandırması başarıyla güncellendi." + } + }, + "products": { + "header": "Ürünler", + "actions": { + "addProducts": "Ürünleri ekle", + "editPrices": "Fiyatları düzenle" + }, + "delete": { + "confirmation_one": "Fiyat listesindeki {{count}} ürün için fiyatları silmek üzeresiniz. Bu işlem geri alınamaz.", + "confirmation_other": "Fiyat listesindeki {{count}} ürün için fiyatları silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast_one": "{{count}} ürün için fiyatlar başarıyla silindi.", + "successToast_other": "{{count}} ürün için fiyatlar başarıyla silindi." + }, + "add": { + "successToast": "Fiyatlar başarıyla fiyat listesine eklendi." + }, + "edit": { + "successToast": "Fiyatlar başarıyla güncellendi." + } + }, + "fields": { + "priceOverrides": { + "label": "Fiyat geçersiz kılmaları", + "header": "Fiyat Geçersiz Kılmaları" + }, + "status": { + "label": "Durum", + "options": { + "active": "Aktif", + "draft": "Taslak", + "expired": "Süresi dolmuş", + "scheduled": "Planlanmış" + } + }, + "type": { + "label": "Tür", + "hint": "Oluşturmak istediğiniz fiyat listesi türünü seçin.", + "options": { + "sale": { + "label": "Satış", + "description": "Satış fiyatları ürünler için geçici fiyat değişiklikleridir." + }, + "override": { + "label": "Geçersiz Kılma", + "description": "Genellikle müşteri özelinde fiyatlar oluşturmak için kullanılır." + } + } + }, + "startsAt": { + "label": "Fiyat listesi bir başlangıç tarihine sahip mi?", + "hint": "Fiyat listesinin gelecekte aktif olmasını planlayın." + }, + "endsAt": { + "label": "Fiyat listesi bir bitiş tarihine sahip mi?", + "hint": "Fiyat listesinin gelecekte devre dışı kalmasını planlayın." + }, + "customerAvailability": { + "header": "Müşteri gruplarını seçin", + "label": "Müşteri uygunluğu", + "hint": "Fiyat listesinin hangi müşteri gruplarına uygulanacağını seçin.", + "placeholder": "Müşteri gruplarını arayın", + "attribute": "Müşteri grupları" + } + } + }, + "profile": { + "domain": "Profil", + "manageYourProfileDetails": "Profil detaylarınızı yönetin.", + "fields": { + "languageLabel": "Dil", + "usageInsightsLabel": "Kullanım istatistikleri" + }, + "edit": { + "header": "Profili Düzenle", + "languageHint": "Yönetim panelinde kullanmak istediğiniz dili seçin. Bu, mağazanızın dilini değiştirmez.", + "languagePlaceholder": "Dili seçin", + "usageInsightsHint": "Kullanım istatistiklerini paylaşarak Medusa'nın iyileştirilmesine yardımcı olun. Topladığımız bilgiler ve nasıl kullandığımız hakkında daha fazla bilgiye <0>dokümantasyonda ulaşabilirsiniz." + }, + "toast": { + "edit": "Profil değişiklikleri kaydedildi" + } + }, + "users": { + "domain": "Kullanıcılar", + "editUser": "Kullanıcıyı Düzenle", + "inviteUser": "Kullanıcı Davet Et", + "inviteUserHint": "Mağazanıza yeni bir kullanıcı davet edin.", + "sendInvite": "Davet gönder", + "pendingInvites": "Bekleyen Davetler", + "deleteInviteWarning": "{{email}} için daveti silmek üzeresiniz. Bu işlem geri alınamaz.", + "resendInvite": "Daveti tekrar gönder", + "copyInviteLink": "Davet bağlantısını kopyala", + "expiredOnDate": "{{date}} tarihinde süresi doldu", + "validFromUntil": "<0>{{from}} - <1>{{until}} arası geçerli", + "acceptedOnDate": "{{date}} tarihinde kabul edildi", + "inviteStatus": { + "accepted": "Kabul edildi", + "pending": "Beklemede", + "expired": "Süresi doldu" + }, + "roles": { + "admin": "Yönetici", + "developer": "Geliştirici", + "member": "Üye" + }, + "deleteUserWarning": "{{name}} kullanıcısını silmek üzeresiniz. Bu işlem geri alınamaz.", + "invite": "Davet Et" + }, + "store": { + "domain": "Mağaza", + "manageYourStoresDetails": "Mağazanızın detaylarını yönetin", + "editStore": "Mağazayı Düzenle", + "defaultCurrency": "Varsayılan para birimi", + "defaultRegion": "Varsayılan bölge", + "swapLinkTemplate": "Değişim bağlantı şablonu", + "paymentLinkTemplate": "Ödeme bağlantı şablonu", + "inviteLinkTemplate": "Davet bağlantı şablonu", + "currencies": "Para Birimleri", + "addCurrencies": "Para birimleri ekle", + "enableTaxInclusivePricing": "Vergi dahil fiyatlandırmayı etkinleştir", + "disableTaxInclusivePricing": "Vergi dahil fiyatlandırmayı devre dışı bırak", + "removeCurrencyWarning_one": "{{count}} para birimini mağazanızdan kaldırmak üzeresiniz. Devam etmeden önce bu para birimiyle tüm fiyatları kaldırdığınızdan emin olun.", + "removeCurrencyWarning_other": "{{count}} para birimini mağazanızdan kaldırmak üzeresiniz. Devam etmeden önce bu para birimleriyle tüm fiyatları kaldırdığınızdan emin olun.", + "currencyAlreadyAdded": "Para birimi mağazanıza zaten eklenmiş.", + "edit": { + "header": "Mağazayı Düzenle" + }, + "toast": { + "update": "Mağaza başarıyla güncellendi", + "currenciesUpdated": "Para birimleri başarıyla güncellendi", + "currenciesRemoved": "Para birimleri mağazadan başarıyla kaldırıldı", + "updatedTaxInclusivitySuccessfully": "Vergi dahil fiyatlandırma başarıyla güncellendi" + } + }, + "regions": { + "domain": "Bölgeler", + "subtitle": "Bir bölge, ürünlerinizi sattığınız bir alandır. Birden fazla ülkeyi kapsayabilir ve farklı vergi oranları, sağlayıcılar ve para birimleri içerebilir.", + "createRegion": "Bölge Oluştur", + "createRegionHint": "Belirli ülkeler için vergi oranlarını ve sağlayıcıları yönetin.", + "addCountries": "Ülkeleri ekle", + "editRegion": "Bölgeyi Düzenle", + "countriesHint": "Bu bölgeye dahil edilecek ülkeleri ekleyin.", + "deleteRegionWarning": "{{name}} bölgesini silmek üzeresiniz. Bu işlem geri alınamaz.", + "removeCountriesWarning_one": "{{count}} ülkeyi bölgeden kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "removeCountriesWarning_other": "{{count}} ülkeyi bölgeden kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "removeCountryWarning": "{{name}} ülkesini bölgeden kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "automaticTaxesHint": "Etkinleştirildiğinde, vergiler yalnızca teslimat adresine göre ödeme sırasında hesaplanır.", + "taxInclusiveHint": "Etkinleştirildiğinde, bölgedeki fiyatlar vergi dahil olacaktır.", + "providersHint": "Bu bölgede kullanılabilir ödeme sağlayıcılarını ekleyin.", + "shippingOptions": "Kargo Seçenekleri", + "deleteShippingOptionWarning": "{{name}} kargo seçeneğini silmek üzeresiniz. Bu işlem geri alınamaz.", + "return": "İade", + "outbound": "Dışa gönderim", + "priceType": "Fiyat Türü", + "flatRate": "Sabit Ücret", + "calculated": "Hesaplanmış", + "list": { + "noRecordsMessage": "Satış yaptığınız bölgeler için bir bölge oluşturun." + }, + "toast": { + "delete": "Bölge başarıyla silindi", + "edit": "Bölge düzenlemesi kaydedildi", + "create": "Bölge başarıyla oluşturuldu", + "countries": "Bölge ülkeleri başarıyla güncellendi" + }, + "shippingOption": { + "createShippingOption": "Kargo Seçeneği Oluştur", + "createShippingOptionHint": "Bölge için yeni bir kargo seçeneği oluşturun.", + "editShippingOption": "Kargo Seçeneğini Düzenle", + "fulfillmentMethod": "Karşılama Yöntemi", + "type": { + "outbound": "Dışa Gönderim", + "outboundHint": "Ürünleri müşteriye göndermek için bir kargo seçeneği oluşturuyorsanız kullanın.", + "return": "İade", + "returnHint": "Müşterinin ürünleri size iade etmesi için bir kargo seçeneği oluşturuyorsanız kullanın." + }, + "priceType": { + "label": "Fiyat Türü", + "flatRate": "Sabit ücret", + "calculated": "Hesaplanmış" + }, + "availability": { + "adminOnly": "Yalnızca yönetici", + "adminOnlyHint": "Etkinleştirildiğinde, kargo seçeneği yalnızca yönetim panelinde kullanılabilir, mağazada değil." + }, + "taxInclusiveHint": "Etkinleştirildiğinde, kargo seçeneğinin fiyatı vergi dahil olacaktır.", + "requirements": { + "label": "Gereksinimler", + "hint": "Kargo seçeneği için gereksinimleri belirleyin." + } + } + }, + "taxes": { + "domain": "Vergi Bölgeleri", + "domainDescription": "Vergi bölgenizi yönetin", + "countries": { + "taxCountriesHint": "Vergi ayarları listelenen ülkelere uygulanır." + }, + "settings": { + "editTaxSettings": "Vergi Ayarlarını Düzenle", + "taxProviderLabel": "Vergi sağlayıcı", + "systemTaxProviderLabel": "Sistem Vergi Sağlayıcı", + "calculateTaxesAutomaticallyLabel": "Vergileri otomatik olarak hesapla", + "calculateTaxesAutomaticallyHint": "Etkinleştirildiğinde, vergi oranları otomatik olarak hesaplanır ve sepete uygulanır. Devre dışı bırakıldığında, vergiler ödeme sırasında manuel olarak hesaplanmalıdır. Manuel vergiler, üçüncü taraf vergi sağlayıcılarıyla kullanım için önerilir.", + "applyTaxesOnGiftCardsLabel": "Hediye kartlarına vergi uygula", + "applyTaxesOnGiftCardsHint": "Etkinleştirildiğinde, hediye kartlarına ödeme sırasında vergi uygulanır. Bazı ülkelerde, hediye kartlarının satın alındığında vergi uygulanmasını gerektiren vergi düzenlemeleri vardır.", + "defaultTaxRateLabel": "Varsayılan vergi oranı", + "defaultTaxCodeLabel": "Varsayılan vergi kodu" + }, + "defaultRate": { + "sectionTitle": "Varsayılan Vergi Oranı" + }, + "taxRate": { + "sectionTitle": "Vergi Oranları", + "createTaxRate": "Vergi Oranı Oluştur", + "createTaxRateHint": "Bölge için yeni bir vergi oranı oluşturun.", + "deleteRateDescription": "{{name}} vergi oranını silmek üzeresiniz. Bu işlem geri alınamaz.", + "editTaxRate": "Vergi Oranını Düzenle", + "editRateAction": "Oranı düzenle", + "editOverridesAction": "Geçersiz kılmaları düzenle", + "editOverridesTitle": "Vergi Oranı Geçersiz Kılmalarını Düzenle", + "editOverridesHint": "Vergi oranı için geçersiz kılmaları belirtin.", + "deleteTaxRateWarning": "{{name}} vergi oranını silmek üzeresiniz. Bu işlem geri alınamaz.", + "productOverridesLabel": "Ürün geçersiz kılmaları", + "productOverridesHint": "Vergi oranı için ürün geçersiz kılmalarını belirtin.", + "addProductOverridesAction": "Ürün geçersiz kılmaları ekle", + "productTypeOverridesLabel": "Ürün türü geçersiz kılmaları", + "productTypeOverridesHint": "Vergi oranı için ürün türü geçersiz kılmalarını belirtin.", + "addProductTypeOverridesAction": "Ürün türü geçersiz kılmaları ekle", + "shippingOptionOverridesLabel": "Kargo seçeneği geçersiz kılmaları", + "shippingOptionOverridesHint": "Vergi oranı için kargo seçeneği geçersiz kılmalarını belirtin.", + "addShippingOptionOverridesAction": "Kargo seçeneği geçersiz kılmaları ekle", + "productOverridesHeader": "Ürünler", + "productTypeOverridesHeader": "Ürün Türleri", + "shippingOptionOverridesHeader": "Kargo Seçenekleri" + } + }, + "locations": { + "domain": "Konumlar", + "editLocation": "Konumu Düzenle", + "addSalesChannels": "Satış kanalları ekle", + "noLocationsFound": "Hiçbir konum bulunamadı", + "selectLocations": "Ürünü stoklayan konumları seçin.", + "deleteLocationWarning": "{{name}} konumunu silmek üzeresiniz. Bu işlem geri alınamaz.", + "removeSalesChannelsWarning_one": "{{count}} satış kanalını konumdan kaldırmak üzeresiniz.", + "removeSalesChannelsWarning_other": "{{count}} satış kanalını konumdan kaldırmak üzeresiniz.", + "toast": { + "create": "Konum başarıyla oluşturuldu", + "update": "Konum başarıyla güncellendi", + "removeChannel": "Satış kanalı başarıyla kaldırıldı" + } + }, + "reservations": { + "domain": "Rezervasyonlar", + "subtitle": "Envanter kalemlerinin ayrılan miktarını yönetin.", + "deleteWarning": "Bir rezervasyonu silmek üzeresiniz. Bu işlem geri alınamaz." + }, + "salesChannels": { + "domain": "Satış Kanalları", + "subtitle": "Ürünlerinizi sattığınız çevrimiçi ve çevrimdışı kanalları yönetin.", + "createSalesChannel": "Satış Kanalı Oluştur", + "createSalesChannelHint": "Ürünlerinizi satmak için yeni bir satış kanalı oluşturun.", + "enabledHint": "Satış kanalının etkin olup olmadığını belirtin.", + "removeProductsWarning_one": "{{sales_channel}}'dan {{count}} ürünü kaldırmak üzeresiniz.", + "removeProductsWarning_other": "{{sales_channel}}'dan {{count}} ürünü kaldırmak üzeresiniz.", + "addProducts": "Ürün Ekle", + "editSalesChannel": "Satış kanalını düzenle", + "productAlreadyAdded": "Ürün zaten satış kanalına eklenmiş.", + "deleteSalesChannelWarning": "{{name}} satış kanalını silmek üzeresiniz. Bu işlem geri alınamaz.", + "toast": { + "create": "Satış kanalı başarıyla oluşturuldu", + "update": "Satış kanalı başarıyla güncellendi", + "delete": "Satış kanalı başarıyla silindi" + }, + "products": { + "list": { + "noRecordsMessage": "Satış kanalında ürün bulunmuyor." + }, + "add": { + "list": { + "noRecordsMessage": "Önce bir ürün oluşturun." + } + } + } + }, + "apiKeyManagement": { + "domain": { + "publishable": "Yayınlanabilir API Anahtarları", + "secret": "Gizli API Anahtarları" + }, + "subtitle": { + "publishable": "Satış kanallarına yönelik taleplerin kapsamını sınırlamak için mağaza vitrini için kullanılan API anahtarlarını yönetin.", + "secret": "Yönetici uygulamalarında yönetici kullanıcıları kimlik doğrulamak için kullanılan API anahtarlarını yönetin." + }, + "status": { + "active": "Aktif", + "revoked": "İptal Edildi" + }, + "type": { + "publishable": "Yayınlanabilir", + "secret": "Gizli" + }, + "create": { + "createPublishableHeader": "Yayınlanabilir API Anahtarı Oluştur", + "createPublishableHint": "Belirli satış kanallarına yönelik taleplerin kapsamını sınırlamak için yeni bir yayınlanabilir API anahtarı oluşturun.", + "createSecretHeader": "Gizli API Anahtarı Oluştur", + "createSecretHint": "Doğrulanmış bir yönetici kullanıcısı olarak Medusa API'ye erişmek için yeni bir gizli API anahtarı oluşturun.", + "secretKeyCreatedHeader": "Gizli Anahtar Oluşturuldu", + "secretKeyCreatedHint": "Yeni gizli anahtarınız oluşturuldu. Şimdi kopyalayın ve güvenli bir şekilde saklayın. Bu anahtar yalnızca bir kez görüntülenecektir.", + "copySecretTokenSuccess": "Gizli anahtar panoya kopyalandı.", + "copySecretTokenFailure": "Gizli anahtar panoya kopyalanamadı.", + "successToast": "API anahtarı başarıyla oluşturuldu." + }, + "edit": { + "header": "API Anahtarını Düzenle", + "description": "API anahtarının başlığını düzenleyin.", + "successToast": "API anahtarı {{title}} başarıyla güncellendi." + }, + "salesChannels": { + "title": "Satış Kanalları Ekle", + "description": "API anahtarının sınırlandırılması gereken satış kanallarını ekleyin.", + "successToast_one": "{{count}} satış kanalı API anahtarına başarıyla eklendi.", + "successToast_other": "{{count}} satış kanalı API anahtarına başarıyla eklendi.", + "alreadyAddedTooltip": "Satış kanalı zaten API anahtarına eklenmiş.", + "list": { + "noRecordsMessage": "Yayınlanabilir API anahtarının kapsamındaki satış kanalı yok." + } + }, + "delete": { + "warning": "{{title}} API anahtarını silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "API anahtarı {{title}} başarıyla silindi." + }, + "revoke": { + "warning": "{{title}} API anahtarını iptal etmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "API anahtarı {{title}} başarıyla iptal edildi." + }, + "addSalesChannels": { + "list": { + "noRecordsMessage": "Önce bir satış kanalı oluşturun." + } + }, + "removeSalesChannel": { + "warning": "Satış kanalı {{name}}'ı API anahtarından kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "warningBatch_one": "{{count}} satış kanalını API anahtarından kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "warningBatch_other": "{{count}} satış kanalını API anahtarından kaldırmak üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "Satış kanalı API anahtarından başarıyla kaldırıldı.", + "successToastBatch_one": "{{count}} satış kanalı API anahtarından başarıyla kaldırıldı.", + "successToastBatch_other": "{{count}} satış kanalı API anahtarından başarıyla kaldırıldı." + }, + "actions": { + "revoke": "API anahtarını iptal et", + "copy": "API anahtarını kopyala", + "copySuccessToast": "API anahtarı panoya kopyalandı." + }, + "table": { + "lastUsedAtHeader": "Son Kullanılma Zamanı", + "createdAtHeader": "İptal Edilme Zamanı" + }, + "fields": { + "lastUsedAtLabel": "Son kullanıldığı zaman", + "revokedByLabel": "İptal eden", + "revokedAtLabel": "İptal zamanı", + "createdByLabel": "Oluşturan" + } + }, + "returnReasons": { + "domain": "İade Nedenleri", + "subtitle": "İade edilen ürünler için nedenleri yönetin.", + "calloutHint": "İadeleri kategorize etmek için nedenleri yönetin.", + "editReason": "İade Nedenini Düzenle", + "create": { + "header": "İade Nedeni Ekle", + "subtitle": "En yaygın iade nedenlerini belirtin.", + "hint": "İadeleri kategorize etmek için yeni bir iade nedeni oluşturun.", + "successToast": "İade nedeni {{label}} başarıyla oluşturuldu." + }, + "edit": { + "header": "İade Nedenini Düzenle", + "subtitle": "İade nedeninin değerini düzenleyin.", + "successToast": "İade nedeni {{label}} başarıyla güncellendi." + }, + "delete": { + "confirmation": "{{label}} iade nedenini silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "İade nedeni {{label}} başarıyla silindi." + }, + "fields": { + "value": { + "label": "Değer", + "placeholder": "yanlış_beden", + "tooltip": "Değer, iade nedeni için benzersiz bir tanımlayıcı olmalıdır." + }, + "label": { + "label": "Etiket", + "placeholder": "Yanlış beden" + }, + "description": { + "label": "Açıklama", + "placeholder": "Müşteri yanlış beden aldı" + } + } + }, + "login": { + "forgotPassword": "Şifrenizi mi unuttunuz? - <0>Sıfırla", + "title": "Medusa'ya Hoş Geldiniz", + "hint": "Hesap alanına erişmek için oturum açın" + }, + "invite": { + "title": "Medusa'ya Hoş Geldiniz", + "hint": "Aşağıda hesabınızı oluşturun", + "backToLogin": "Girişe geri dön", + "createAccount": "Hesap oluştur", + "alreadyHaveAccount": "Zaten bir hesabınız var mı? - <0>Giriş Yap", + "emailTooltip": "E-postanız değiştirilemez. Başka bir e-posta kullanmak istiyorsanız, yeni bir davet gönderilmelidir.", + "invalidInvite": "Davet geçersiz veya süresi dolmuş.", + "successTitle": "Hesabınız kaydedildi", + "successHint": "Hemen Medusa Yönetici ile başlayın.", + "successAction": "Medusa Yöneticiye Başla", + "invalidTokenTitle": "Davet tokeniniz geçersiz", + "invalidTokenHint": "Yeni bir davet bağlantısı talep etmeyi deneyin.", + "passwordMismatch": "Şifreler uyuşmuyor", + "toast": { + "accepted": "Davet başarıyla kabul edildi" + } + }, + "resetPassword": { + "title": "Şifreyi sıfırla", + "hint": "Aşağıya e-postanızı girin, şifrenizi nasıl sıfırlayacağınızı anlatan talimatları size göndereceğiz.", + "email": "E-posta", + "sendResetInstructions": "Sıfırlama talimatlarını gönder", + "backToLogin": "<0>Girişe geri dön", + "newPasswordHint": "Aşağıda yeni bir şifre seçin.", + "invalidTokenTitle": "Şifre sıfırlama tokeniniz geçersiz", + "invalidTokenHint": "Yeni bir sıfırlama bağlantısı talep etmeyi deneyin.", + "expiredTokenTitle": "Şifre sıfırlama tokeninizin süresi dolmuş", + "goToResetPassword": "Şifre Sıfırla'ya Git", + "resetPassword": "Şifreyi sıfırla", + "newPassword": "Yeni şifre", + "repeatNewPassword": "Yeni şifreyi tekrar et", + "tokenExpiresIn": "Token <0>{{time}} dakika içinde sona eriyor", + "successfulRequestTitle": "Size başarıyla bir e-posta gönderdik", + "successfulRequest": "Şifrenizi sıfırlamak için kullanabileceğiniz bir e-posta gönderdik. Birkaç dakika içinde almazsanız, spam klasörünüzü kontrol edin.", + "successfulResetTitle": "Şifre sıfırlama başarılı", + "successfulReset": "Lütfen giriş sayfasında oturum açın.", + "passwordMismatch": "Şifreler uyuşmuyor", + "invalidLinkTitle": "Şifre sıfırlama bağlantınız geçersiz", + "invalidLinkHint": "Şifrenizi yeniden sıfırlamayı deneyin." + }, + "workflowExecutions": { + "domain": "İş Akışları", + "subtitle": "Medusa uygulamanızdaki iş akışı yürütmelerini görüntüleyin ve takip edin.", + "transactionIdLabel": "İşlem Kimliği", + "workflowIdLabel": "İş Akışı Kimliği", + "progressLabel": "İlerleme", + "stepsCompletedLabel_one": "{{count}} adımın {{completed}}'i tamamlandı", + "stepsCompletedLabel_other": "{{count}} adımın {{completed}}'i tamamlandı", + "list": { + "noRecordsMessage": "Henüz herhangi bir iş akışı yürütülmedi." + }, + "history": { + "sectionTitle": "Geçmiş", + "runningState": "Çalışıyor...", + "awaitingState": "Bekleniyor", + "failedState": "Başarısız", + "skippedState": "Atlandı", + "skippedFailureState": "Atlandı (Başarısızlık)", + "definitionLabel": "Tanım", + "outputLabel": "Çıktı", + "compensateInputLabel": "Telafi girdisi", + "revertedLabel": "Geri alındı", + "errorLabel": "Hata" + }, + "state": { + "done": "Tamamlandı", + "failed": "Başarısız", + "reverted": "Geri alındı", + "invoking": "Çağırılıyor", + "compensating": "Telafi ediliyor", + "notStarted": "Başlamadı" + }, + "transaction": { + "state": { + "waitingToCompensate": "Telafi için bekliyor" + } + }, + "step": { + "state": { + "skipped": "Atlandı", + "skippedFailure": "Atlandı (Başarısızlık)", + "dormant": "Beklemede", + "timeout": "Zaman aşımı" + } + } + }, + "productTypes": { + "domain": "Ürün Türleri", + "subtitle": "Ürünlerinizi türlere göre organize edin.", + "create": { + "header": "Ürün Türü Oluştur", + "hint": "Ürünlerinizi kategorize etmek için yeni bir ürün türü oluşturun.", + "successToast": "Ürün türü {{value}} başarıyla oluşturuldu." + }, + "edit": { + "header": "Ürün Türünü Düzenle", + "successToast": "Ürün türü {{value}} başarıyla güncellendi." + }, + "delete": { + "confirmation": "{{value}} ürün türünü silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "Ürün türü {{value}} başarıyla silindi." + }, + "fields": { + "value": "Değer" + } + }, + "productTags": { + "domain": "Ürün Etiketleri", + "create": { + "header": "Ürün Etiketi Oluştur", + "subtitle": "Ürünlerinizi kategorize etmek için yeni bir ürün etiketi oluşturun.", + "successToast": "Ürün etiketi {{value}} başarıyla oluşturuldu." + }, + "edit": { + "header": "Ürün Etiketini Düzenle", + "subtitle": "Ürün etiketinin değerini düzenleyin.", + "successToast": "Ürün etiketi {{value}} başarıyla güncellendi." + }, + "delete": { + "confirmation": "{{value}} ürün etiketini silmek üzeresiniz. Bu işlem geri alınamaz.", + "successToast": "Ürün etiketi {{value}} başarıyla silindi." + }, + "fields": { + "value": "Değer" + } + }, + "notifications": { + "domain": "Bildirimler", + "emptyState": { + "title": "Bildirim yok", + "description": "Şu anda herhangi bir bildiriminiz yok, ancak olduğunda burada görünecekler." + }, + "accessibility": { + "description": "Medusa aktiviteleri hakkında bildirimler burada listelenecek." + } + }, + "errors": { + "serverError": "Sunucu hatası - Daha sonra tekrar deneyin.", + "invalidCredentials": "Yanlış e-posta veya şifre" + }, + "statuses": { + "scheduled": "Planlandı", + "expired": "Süresi Dolmuş", + "active": "Aktif", + "enabled": "Etkin", + "disabled": "Devre Dışı" + }, + "labels": { + "productVariant": "Ürün Varyantı", + "prices": "Fiyatlar", + "available": "Mevcut", + "inStock": "Stokta Var", + "added": "Eklendi", + "removed": "Kaldırıldı" + }, + "fields": { + "amount": "Miktar", + "refundAmount": "İade miktarı", + "name": "İsim", + "default": "Varsayılan", + "lastName": "Soyadı", + "firstName": "Adı", + "title": "Başlık", + "customTitle": "Özel başlık", + "manageInventory": "Envanteri yönet", + "inventoryKit": "Envanter seti var", + "inventoryItems": "Envanter öğeleri", + "inventoryItem": "Envanter öğesi", + "requiredQuantity": "Gerekli miktar", + "description": "Açıklama", + "email": "E-posta", + "password": "Şifre", + "repeatPassword": "Şifreyi Tekrarla", + "confirmPassword": "Şifreyi Onayla", + "newPassword": "Yeni Şifre", + "repeatNewPassword": "Yeni Şifreyi Tekrarla", + "categories": "Kategoriler", + "shippingMethod": "Kargo metodu", + "configurations": "Yapılandırmalar", + "conditions": "Koşullar", + "category": "Kategori", + "collection": "Koleksiyon", + "discountable": "İndirim uygulanabilir", + "handle": "Tanıtıcı", + "subtitle": "Alt başlık", + "item": "Öğe", + "qty": "adet.", + "limit": "Limit", + "tags": "Etiketler", + "type": "Tür", + "reason": "Sebep", + "none": "hiçbiri", + "all": "tümü", + "search": "Ara", + "percentage": "Yüzde", + "sales_channels": "Satış Kanalları", + "customer_groups": "Müşteri Grupları", + "product_tags": "Ürün Etiketleri", + "product_types": "Ürün Türleri", + "product_collections": "Ürün Koleksiyonları", + "status": "Durum", + "code": "Kod", + "value": "Değer", + "disabled": "Devre dışı", + "dynamic": "Dinamik", + "normal": "Normal", + "years": "Yıllar", + "months": "Aylar", + "days": "Günler", + "hours": "Saatler", + "minutes": "Dakikalar", + "totalRedemptions": "Toplam Kullanım", + "countries": "Ülkeler", + "paymentProviders": "Ödeme Sağlayıcıları", + "refundReason": "İade Sebebi", + "fulfillmentProviders": "Tamamlama Sağlayıcıları", + "fulfillmentProvider": "Tamamlama Sağlayıcısı", + "providers": "Sağlayıcılar", + "availability": "Mevcudiyet", + "inventory": "Envanter", + "optional": "Opsiyonel", + "note": "Not", + "automaticTaxes": "Otomatik Vergiler", + "taxInclusivePricing": "Vergi dahil fiyatlandırma", + "currency": "Para Birimi", + "address": "Adres", + "address2": "Daire, apartman vb.", + "city": "Şehir", + "postalCode": "Posta Kodu", + "country": "Ülke", + "state": "Eyalet", + "province": "İl", + "company": "Şirket", + "phone": "Telefon", + "metadata": "Meta veri", + "selectCountry": "Ülke seçin", + "products": "Ürünler", + "variants": "Varyantlar", + "orders": "Siparişler", + "account": "Hesap", + "total": "Toplam Sipariş", + "paidTotal": "Toplam tahsilat", + "totalExclTax": "Vergi hariç toplam", + "subtotal": "Ara toplam", + "shipping": "Kargo", + "outboundShipping": "Giden Kargo", + "returnShipping": "İade Kargosu", + "tax": "Vergi", + "created": "Oluşturuldu", + "key": "Anahtar", + "customer": "Müşteri", + "date": "Tarih", + "order": "Sipariş", + "fulfillment": "Tamamlama", + "provider": "Sağlayıcı", + "payment": "Ödeme", + "items": "Öğeler", + "salesChannel": "Satış Kanalı", + "region": "Bölge", + "discount": "İndirim", + "role": "Rol", + "sent": "Gönderildi", + "salesChannels": "Satış Kanalları", + "product": "Ürün", + "createdAt": "Oluşturulma", + "updatedAt": "Güncellenme", + "revokedAt": "Geri alınma", + "true": "Doğru", + "false": "Yanlış", + "giftCard": "Hediye Kartı", + "tag": "Etiket", + "dateIssued": "Düzenlenme Tarihi", + "issuedDate": "Düzenleme Tarihi", + "expiryDate": "Son Kullanma Tarihi", + "price": "Fiyat", + "priceTemplate": "Fiyat {{regionOrCurrency}}", + "height": "Yükseklik", + "width": "Genişlik", + "length": "Uzunluk", + "weight": "Ağırlık", + "midCode": "MID kodu", + "hsCode": "HS kodu", + "ean": "EAN", + "upc": "UPC", + "inventoryQuantity": "Envanter miktarı", + "barcode": "Barkod", + "countryOfOrigin": "Menşei ülke", + "material": "Malzeme", + "thumbnail": "Küçük resim", + "sku": "SKU", + "managedInventory": "Yönetilen envanter", + "allowBackorder": "Ön siparişe izin ver", + "inStock": "Stokta var", + "location": "Konum", + "quantity": "Miktar", + "variant": "Varyant", + "id": "ID", + "parent": "Ebeveyn", + "minSubtotal": "Minimum Ara Toplam", + "maxSubtotal": "Maksimum Ara Toplam", + "shippingProfile": "Kargo Profili", + "summary": "Özet", + "details": "Detaylar", + "label": "Etiket", + "rate": "Oran", + "requiresShipping": "Kargo gerektirir", + "unitPrice": "Birim fiyat", + "startDate": "Başlangıç Tarihi", + "endDate": "Bitiş Tarihi", + "draft": "Taslak", + "values": "Değerler" + }, + "dateTime": { + "years_one": "Yıl", + "years_other": "Yıllar", + "months_one": "Ay", + "months_other": "Aylar", + "weeks_one": "Hafta", + "weeks_other": "Haftalar", + "days_one": "Gün", + "days_other": "Günler", + "hours_one": "Saat", + "hours_other": "Saatler", + "minutes_one": "Dakika", + "minutes_other": "Dakikalar", + "seconds_one": "Saniye", + "seconds_other": "Saniyeler" + } +} From f5c580a661fd0e04287369120ee70ce269614367 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 18 Nov 2024 09:52:00 +0200 Subject: [PATCH 05/14] docs: add missing prerequisite in stripe's storefront integration (#10097) --- .../checkout/payment/stripe/page.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/www/apps/resources/app/storefront-development/checkout/payment/stripe/page.mdx b/www/apps/resources/app/storefront-development/checkout/payment/stripe/page.mdx index 9313518db1beb..8085fd1616f84 100644 --- a/www/apps/resources/app/storefront-development/checkout/payment/stripe/page.mdx +++ b/www/apps/resources/app/storefront-development/checkout/payment/stripe/page.mdx @@ -23,6 +23,10 @@ For other types of storefronts, the steps are similar. However, refer to [Stripe text: "Stripe publishable API key.", link: "https://support.stripe.com/questions/locate-api-keys-in-the-dashboard" }, + { + text: "Cart context in your storefront, which is used in a code snippet later.", + link: "/resources/storefront-development/cart/context" + }, ]} /> ## 1. Install Stripe SDK @@ -57,6 +61,12 @@ For Next.js storefronts, the environment variable's name must be prefixed with ` Then, create a file holding the following Stripe component: + + +This snippet assumes you're using the provider from the [Cart Context guide](../../../cart/context/page.mdx) in your storefront. + + + export const highlights = [ ["10", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."], ["13", "stripePromise", "Initialize stripe using the environment variable added in the previous step."], From 2c957c64be3302e0939be10986184dd7532065e9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 18 Nov 2024 14:58:30 +0530 Subject: [PATCH 06/14] refactor: rename workflow to singular (#10134) --- .../order/workflows/{create-orders.ts => create-order.ts} | 8 +++++++- packages/core/core-flows/src/order/workflows/index.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) rename packages/core/core-flows/src/order/workflows/{create-orders.ts => create-order.ts} (96%) diff --git a/packages/core/core-flows/src/order/workflows/create-orders.ts b/packages/core/core-flows/src/order/workflows/create-order.ts similarity index 96% rename from packages/core/core-flows/src/order/workflows/create-orders.ts rename to packages/core/core-flows/src/order/workflows/create-order.ts index 5fd35f5fa92a6..80a015051143c 100644 --- a/packages/core/core-flows/src/order/workflows/create-orders.ts +++ b/packages/core/core-flows/src/order/workflows/create-order.ts @@ -88,7 +88,7 @@ export const createOrdersWorkflowId = "create-orders" /** * This workflow creates an order. */ -export const createOrdersWorkflow = createWorkflow( +export const createOrderWorkflow = createWorkflow( createOrdersWorkflowId, (input: WorkflowData) => { const variantIds = transform({ input }, (data) => { @@ -189,3 +189,9 @@ export const createOrdersWorkflow = createWorkflow( }) } ) + +/** + * @deprecated + * Instead use the singular name "createOrderWorkflow" + */ +export const createOrdersWorkflow = createOrderWorkflow diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index f4eefc28ae74b..a68f9c2a8ffc5 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -22,7 +22,7 @@ export * from "./create-fulfillment" export * from "./create-order-change" export * from "./create-order-change-actions" export * from "./create-order-payment-collection" -export * from "./create-orders" +export * from "./create-order" export * from "./create-shipment" export * from "./decline-order-change" export * from "./delete-order-change" From d933b3f1e4a92e841e881a1fd173df806008cc4a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 18 Nov 2024 15:31:29 +0530 Subject: [PATCH 07/14] Refactor/finish rename order (#10136) --- .changeset/neat-geese-allow.md | 6 ++++++ packages/medusa/src/api/admin/draft-orders/route.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/neat-geese-allow.md diff --git a/.changeset/neat-geese-allow.md b/.changeset/neat-geese-allow.md new file mode 100644 index 0000000000000..31cd3abe248b8 --- /dev/null +++ b/.changeset/neat-geese-allow.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/core-flows": patch +--- + +Refactor/finish rename order diff --git a/packages/medusa/src/api/admin/draft-orders/route.ts b/packages/medusa/src/api/admin/draft-orders/route.ts index 2af2c958e4a73..3c92c7295315b 100644 --- a/packages/medusa/src/api/admin/draft-orders/route.ts +++ b/packages/medusa/src/api/admin/draft-orders/route.ts @@ -1,4 +1,4 @@ -import { createOrdersWorkflow } from "@medusajs/core-flows" +import { createOrderWorkflow } from "@medusajs/core-flows" import { ContainerRegistrationKeys, OrderStatus, @@ -83,7 +83,7 @@ export const POST = async ( input.email = customer?.email } - const { result } = await createOrdersWorkflow(req.scope).run({ + const { result } = await createOrderWorkflow(req.scope).run({ input: workflowInput, }) From 47ca1d4b54de78fe952619a7d331ea4b0ab3d7a6 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:55:22 -0300 Subject: [PATCH 08/14] fix(inventory): update reservation quantity (#10139) --- .changeset/curvy-spies-design.md | 5 +++++ .../__tests__/inventory-module-service.spec.ts | 11 ++++++++++- .../inventory/src/services/inventory-module.ts | 10 +++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .changeset/curvy-spies-design.md diff --git a/.changeset/curvy-spies-design.md b/.changeset/curvy-spies-design.md new file mode 100644 index 0000000000000..a0818d3b29b2a --- /dev/null +++ b/.changeset/curvy-spies-design.md @@ -0,0 +1,5 @@ +--- +"@medusajs/inventory": patch +--- + +Update reservation quantity diff --git a/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts b/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts index 5034380a39280..89eb399d1ef46 100644 --- a/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts +++ b/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts @@ -399,6 +399,15 @@ moduleIntegrationTestRunner({ const updated = await service.updateReservationItems(update) expect(updated).toEqual(expect.objectContaining(update)) + + const update2 = { + id: reservationItem.id, + quantity: 10, + } + + const updated2 = await service.updateReservationItems(update2) + + expect(updated2).toEqual(expect.objectContaining(update2)) }) it("should adjust reserved_quantity of inventory level after updates increasing reserved quantity", async () => { @@ -438,7 +447,7 @@ moduleIntegrationTestRunner({ it("should throw error when increasing reserved quantity beyond availability", async () => { const update = { id: reservationItem.id, - quantity: 10, + quantity: 11, } const error = await service diff --git a/packages/modules/inventory/src/services/inventory-module.ts b/packages/modules/inventory/src/services/inventory-module.ts index 688b745ada724..e3ac99cf439f6 100644 --- a/packages/modules/inventory/src/services/inventory-module.ts +++ b/packages/modules/inventory/src/services/inventory-module.ts @@ -746,10 +746,18 @@ export default class InventoryModuleService const availabilityData = input.map((data) => { const reservation = reservationMap.get(data.id)! + let adjustment = data.quantity + ? MathBN.sub(data.quantity, reservation.quantity) + : 0 + + if (MathBN.lt(adjustment, 0)) { + adjustment = 0 + } + return { inventory_item_id: reservation.inventory_item_id, location_id: data.location_id ?? reservation.location_id, - quantity: data.quantity ?? reservation.quantity, + quantity: adjustment, allow_backorder: data.allow_backorder || reservation.allow_backorder || false, } From b1b7a4abf10956d2d2863ba2b7e08e39b1abfbc1 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 18 Nov 2024 18:45:13 +0100 Subject: [PATCH 09/14] fix(payment): Idempotent cancellation and proper creationg fail handling (#10135) RESOLVES SUP-188 **What** Two changes are happening here - In the stripe payment provider, idempotent cancellation action, if not id is provided then return the existing data unchanged - Payment module should not try to cancel a session that have failed to be created in the first place --- .changeset/selfish-poems-jog.md | 6 ++ .../services/payment-module/index.spec.ts | 81 ++++++++++++++++++- .../payment/src/services/payment-module.ts | 26 +++--- .../payment-stripe/src/core/stripe-base.ts | 5 ++ 4 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 .changeset/selfish-poems-jog.md diff --git a/.changeset/selfish-poems-jog.md b/.changeset/selfish-poems-jog.md new file mode 100644 index 0000000000000..ffb43be1bd671 --- /dev/null +++ b/.changeset/selfish-poems-jog.md @@ -0,0 +1,6 @@ +--- +"@medusajs/payment": patch +"@medusajs/payment-stripe": patch +--- + +fix(payment): Idempotent cancellation and proper creationg fail handling diff --git a/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index 7a09a1cfabb12..8ea2d350951dd 100644 --- a/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -14,6 +14,10 @@ moduleIntegrationTestRunner({ moduleName: Modules.PAYMENT, testSuite: ({ MikroOrmWrapper, service }) => { describe("Payment Module Service", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + it(`should export the appropriate linkable configuration`, () => { const linkable = Module(Modules.PAYMENT, { service: PaymentModuleService, @@ -395,7 +399,6 @@ moduleIntegrationTestRunner({ customer: {}, billing_address: {}, email: "test@test.test.com", - resource_id: "cart_test", }, }) @@ -422,6 +425,80 @@ moduleIntegrationTestRunner({ }) ) }) + + it("should gracefully handle payment session creation fails from external provider", async () => { + jest + .spyOn((service as any).paymentProviderService_, "createSession") + .mockImplementationOnce(() => { + throw new Error("Create session error") + }) + + const deleteProviderSessionMock = jest.spyOn( + (service as any).paymentProviderService_, + "deleteSession" + ) + + const deletePaymentSessionMock = jest.spyOn( + (service as any).paymentSessionService_, + "delete" + ) + + const error = await service + .createPaymentSession("pay-col-id-1", { + provider_id: "pp_system_default", + amount: 200, + currency_code: "usd", + data: {}, + context: { + extra: {}, + customer: {}, + billing_address: {}, + email: "test@test.test.com", + }, + }) + .catch((e) => e) + + expect(deleteProviderSessionMock).toHaveBeenCalledTimes(0) + expect(deletePaymentSessionMock).toHaveBeenCalledTimes(1) + expect(error.message).toEqual("Create session error") + }) + + it("should gracefully handle payment session creation fails from internal failure", async () => { + jest + .spyOn((service as any).paymentSessionService_, "update") + .mockImplementationOnce(() => { + throw new Error("Update session error") + }) + + const deleteProviderSessionMock = jest.spyOn( + (service as any).paymentProviderService_, + "deleteSession" + ) + + const deletePaymentSessionMock = jest.spyOn( + (service as any).paymentSessionService_, + "delete" + ) + + const error = await service + .createPaymentSession("pay-col-id-1", { + provider_id: "pp_system_default", + amount: 200, + currency_code: "usd", + data: {}, + context: { + extra: {}, + customer: {}, + billing_address: {}, + email: "test@test.test.com", + }, + }) + .catch((e) => e) + + expect(deleteProviderSessionMock).toHaveBeenCalledTimes(1) + expect(deletePaymentSessionMock).toHaveBeenCalledTimes(1) + expect(error.message).toEqual("Update session error") + }) }) describe("update", () => { @@ -436,7 +513,6 @@ moduleIntegrationTestRunner({ customer: {}, billing_address: {}, email: "test@test.test.com", - resource_id: "cart_test", }, }) @@ -446,7 +522,6 @@ moduleIntegrationTestRunner({ currency_code: "eur", data: {}, context: { - resource_id: "res_id", extra: {}, customer: {}, billing_address: {}, diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index d27487f45c9e6..bc24d4ea7d6d0 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -299,6 +299,7 @@ export default class PaymentModuleService @MedusaContext() sharedContext?: Context ): Promise { let paymentSession: PaymentSession | undefined + let providerPaymentSession: Record | undefined try { paymentSession = await this.createPaymentSession_( @@ -307,30 +308,33 @@ export default class PaymentModuleService sharedContext ) - const providerSessionSession = - await this.paymentProviderService_.createSession(input.provider_id, { + providerPaymentSession = await this.paymentProviderService_.createSession( + input.provider_id, + { context: { ...input.context, session_id: paymentSession.id }, amount: input.amount, currency_code: input.currency_code, - }) + } + ) paymentSession = ( await this.paymentSessionService_.update( { id: paymentSession.id, - data: { ...input.data, ...providerSessionSession }, + data: { ...input.data, ...providerPaymentSession }, }, sharedContext ) )[0] } catch (error) { - if (paymentSession) { - // In case the session is created, but fails to be updated in Medusa, - // we catch the error and delete the session and rethrow. + if (providerPaymentSession) { await this.paymentProviderService_.deleteSession({ provider_id: input.provider_id, data: input.data, }) + } + + if (paymentSession) { await this.paymentSessionService_.delete( paymentSession.id, sharedContext @@ -340,9 +344,7 @@ export default class PaymentModuleService throw error } - return await this.baseRepository_.serialize(paymentSession, { - populate: true, - }) + return await this.baseRepository_.serialize(paymentSession) } @InjectTransactionManager() @@ -573,9 +575,7 @@ export default class PaymentModuleService // NOTE: currently there is no update with the provider but maybe data could be updated const result = await this.paymentService_.update(data, sharedContext) - return await this.baseRepository_.serialize(result[0], { - populate: true, - }) + return await this.baseRepository_.serialize(result[0]) } @InjectManager() diff --git a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts index 8c51410911e82..9c1cc73f8d600 100644 --- a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts +++ b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts @@ -186,6 +186,11 @@ abstract class StripeBase extends AbstractPaymentProvider { ): Promise { try { const id = paymentSessionData.id as string + + if (!id) { + return paymentSessionData + } + return (await this.stripe_.paymentIntents.cancel( id )) as unknown as PaymentProviderSessionResponse["data"] From 36460a3a07e9906def642b7b8d10e940da2c7eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:53:22 +0100 Subject: [PATCH 10/14] feat(medusa, types, utils, core-flows, order) request & accept order transfer (#10106) **What** - add request order transfer workflow - add admin endpoint for transferring an order to a customer - accept order transfer storefront endpoint - accept transfer workflow - changes in the order module to introduce new change and action types --- **Note** - we return 400 instead 409 currently if there is already an active order edit, I will revisit this in a followup - endpoint for requesting order transfer from the storefront will be added in a separate PR --- RESOLVES CMRC-701 RESOLVES CMRC-703 RESOLVES CMRC-704 RESOLVES CMRC-705 --- .../order/admin/transfer-flow.spec.ts | 233 ++++++++++++++++++ .../core-flows/src/order/workflows/index.ts | 2 + .../transfer/accept-order-transfer.ts | 109 ++++++++ .../transfer/request-order-transfer.ts | 136 ++++++++++ packages/core/types/src/order/common.ts | 3 +- packages/core/types/src/order/mutations.ts | 1 + .../src/workflow/order/accept-transfer.ts | 4 + .../core/types/src/workflow/order/index.ts | 2 + .../src/workflow/order/request-transfer.ts | 8 + packages/core/utils/src/core-flows/events.ts | 2 + .../utils/src/order/order-change-action.ts | 1 + .../api/admin/orders/[id]/transfer/route.ts | 39 +++ .../src/api/admin/orders/middlewares.ts | 12 + .../medusa/src/api/admin/orders/validators.ts | 7 + .../orders/[id]/transfer/accept/route.ts | 29 +++ .../src/api/store/orders/middlewares.ts | 22 +- .../medusa/src/api/store/orders/validators.ts | 8 + .../modules/order/src/types/utils/index.ts | 2 + .../modules/order/src/utils/actions/index.ts | 1 + .../src/utils/actions/transfer-customer.ts | 19 ++ .../order/src/utils/apply-order-changes.ts | 24 +- 21 files changed, 660 insertions(+), 4 deletions(-) create mode 100644 integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts create mode 100644 packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts create mode 100644 packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts create mode 100644 packages/core/types/src/workflow/order/accept-transfer.ts create mode 100644 packages/core/types/src/workflow/order/request-transfer.ts create mode 100644 packages/medusa/src/api/admin/orders/[id]/transfer/route.ts create mode 100644 packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts create mode 100644 packages/modules/order/src/utils/actions/transfer-customer.ts diff --git a/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts new file mode 100644 index 0000000000000..51fd210b19a66 --- /dev/null +++ b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts @@ -0,0 +1,233 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { + adminHeaders, + createAdminUser, + generatePublishableKey, + generateStoreHeaders, +} from "../../../../helpers/create-admin-user" +import { createOrderSeeder } from "../../fixtures/order" + +jest.setTimeout(300000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + let order + let customer + let user + let storeHeaders + + beforeEach(async () => { + const container = getContainer() + + user = (await createAdminUser(dbConnection, adminHeaders, container)).user + const publishableKey = await generatePublishableKey(container) + storeHeaders = generateStoreHeaders({ publishableKey }) + + const seeders = await createOrderSeeder({ api, container }) + + const registeredCustomerToken = ( + await api.post("/auth/customer/emailpass/register", { + email: "test@email.com", + password: "password", + }) + ).data.token + + customer = ( + await api.post( + "/store/customers", + { + email: "test@email.com", + }, + { + headers: { + Authorization: `Bearer ${registeredCustomerToken}`, + ...storeHeaders.headers, + }, + } + ) + ).data.customer + + order = seeders.order + }) + + describe("Transfer Order flow", () => { + it("should pass order transfer flow from admin successfully", async () => { + // 1. Admin requests order transfer for a customer with an account + await api.post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + + const orderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=+customer_id,+email`, + adminHeaders + ) + ).data.order + + // 2. Order still belongs to the guest customer since the transfer hasn't been accepted yet + expect(orderResult.email).toEqual("tony@stark-industries.com") + expect(orderResult.customer_id).not.toEqual(customer.id) + + const orderPreviewResult = ( + await api.get(`/admin/orders/${order.id}/preview`, adminHeaders) + ).data.order + + expect(orderPreviewResult).toEqual( + expect.objectContaining({ + customer_id: customer.id, + order_change: expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: user.id, + }), + }) + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(1) + expect(orderChangesResult[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: user.id, + created_by: user.id, + confirmed_by: null, + confirmed_at: null, + declined_by: null, + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 2, + action: "TRANSFER_CUSTOMER", + reference: "customer", + reference_id: customer.id, + details: expect.objectContaining({ + token: expect.any(String), + original_email: "tony@stark-industries.com", + }), + }), + ]), + }) + ) + + // 3. Guest customer who received the token accepts the transfer + await api.post( + `/store/orders/${order.id}/transfer/accept`, + { token: orderChangesResult[0].actions[0].details.token }, + { + headers: { + ...storeHeaders.headers, + }, + } + ) + + const finalOrderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=+customer_id,+email`, + adminHeaders + ) + ).data.order + + expect(finalOrderResult.email).toEqual("tony@stark-industries.com") + // 4. Customer account is now associated with the order (email on the order is still as original, guest email) + expect(finalOrderResult.customer_id).toEqual(customer.id) + }) + + it("should fail to request order transfer to a guest customer", async () => { + const customer = ( + await api.post( + "/admin/customers", + { + first_name: "guest", + email: "guest@medusajs.com", + }, + adminHeaders + ) + ).data.customer + + const err = await api + .post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + .catch((e) => e) + + expect(err.response.status).toBe(400) + expect(err.response.data).toEqual( + expect.objectContaining({ + type: "invalid_data", + message: `Cannot transfer order: ${order.id} to a guest customer account: guest@medusajs.com`, + }) + ) + }) + + it("should fail to accept order transfer with invalid token", async () => { + await api.post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(1) + expect(orderChangesResult[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: user.id, + created_by: user.id, + confirmed_by: null, + confirmed_at: null, + declined_by: null, + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 2, + action: "TRANSFER_CUSTOMER", + reference: "customer", + reference_id: customer.id, + details: expect.objectContaining({ + token: expect.any(String), + original_email: "tony@stark-industries.com", + }), + }), + ]), + }) + ) + + const err = await api + .post( + `/store/orders/${order.id}/transfer/accept`, + { token: "fake-token" }, + { + headers: { + ...storeHeaders.headers, + }, + } + ) + .catch((e) => e) + + expect(err.response.status).toBe(400) + expect(err.response.data).toEqual( + expect.objectContaining({ + type: "not_allowed", + message: `Invalid token.`, + }) + ) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index a68f9c2a8ffc5..e7950a5d04f87 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -78,3 +78,5 @@ export * from "./return/update-return-shipping-method" export * from "./update-order-change-actions" export * from "./update-order-changes" export * from "./update-tax-lines" +export * from "./transfer/request-order-transfer" +export * from "./transfer/accept-order-transfer" diff --git a/packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts new file mode 100644 index 0000000000000..ccbe4d78cd7d2 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts @@ -0,0 +1,109 @@ +import { + OrderChangeDTO, + OrderDTO, + OrderWorkflow, +} from "@medusajs/framework/types" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { OrderPreviewDTO } from "@medusajs/types" + +import { useRemoteQueryStep } from "../../../common" +import { throwIfOrderIsCancelled } from "../../utils/order-validation" +import { previewOrderChangeStep } from "../../steps" +import { + ChangeActionType, + MedusaError, + OrderChangeStatus, +} from "@medusajs/utils" +import { confirmOrderChanges } from "../../steps/confirm-order-changes" + +/** + * This step validates that an order transfer can be accepted. + */ +export const acceptOrderTransferValidationStep = createStep( + "accept-order-transfer-validation", + async function ({ + token, + order, + orderChange, + }: { + token: string + order: OrderDTO + orderChange: OrderChangeDTO + }) { + throwIfOrderIsCancelled({ order }) + + if (!orderChange || orderChange.change_type !== "transfer") { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order ${order.id} does not have an order transfer request.` + ) + } + const transferCustomerAction = orderChange.actions.find( + (a) => a.action === ChangeActionType.TRANSFER_CUSTOMER + ) + + if (!token.length || token !== transferCustomerAction?.details!.token) { + throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Invalid token.") + } + } +) + +export const acceptOrderTransferWorkflowId = "accept-order-transfer-workflow" +/** + * This workflow accepts an order transfer. + */ +export const acceptOrderTransferWorkflow = createWorkflow( + acceptOrderTransferWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "email", "status", "customer_id"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: [ + "id", + "status", + "change_type", + "actions.id", + "actions.order_id", + "actions.action", + "actions.details", + "actions.reference", + "actions.reference_id", + "actions.internal_note", + ], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + acceptOrderTransferValidationStep({ + order, + orderChange, + token: input.token, + }) + + confirmOrderChanges({ + changes: [orderChange], + orderId: order.id, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts new file mode 100644 index 0000000000000..cee99c313fb46 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts @@ -0,0 +1,136 @@ +import { OrderDTO, OrderWorkflow } from "@medusajs/framework/types" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { CustomerDTO, OrderPreviewDTO } from "@medusajs/types" +import { v4 as uid } from "uuid" + +import { emitEventStep, useRemoteQueryStep } from "../../../common" +import { createOrderChangeStep } from "../../steps/create-order-change" +import { throwIfOrderIsCancelled } from "../../utils/order-validation" +import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" +import { + ChangeActionType, + MedusaError, + OrderChangeStatus, + OrderWorkflowEvents, +} from "@medusajs/utils" +import { previewOrderChangeStep, updateOrderChangesStep } from "../../steps" + +/** + * This step validates that an order transfer can be requested. + */ +export const requestOrderTransferValidationStep = createStep( + "request-order-transfer-validation", + async function ({ + order, + customer, + }: { + order: OrderDTO + customer: CustomerDTO + }) { + throwIfOrderIsCancelled({ order }) + + if (!customer.has_account) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot transfer order: ${order.id} to a guest customer account: ${customer.email}` + ) + } + + if (order.customer_id === customer.id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order: ${order.id} already belongs to customer: ${customer.id}` + ) + } + } +) + +export const requestOrderTransferWorkflowId = "request-order-transfer-workflow" +/** + * This workflow requests an order transfer. + */ +export const requestOrderTransferWorkflow = createWorkflow( + requestOrderTransferWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "email", "status", "customer_id"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + const customer: CustomerDTO = useRemoteQueryStep({ + entry_point: "customers", + fields: ["id", "email", "has_account"], + variables: { id: input.customer_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "customer-query" }) + + requestOrderTransferValidationStep({ order, customer }) + + const orderChangeInput = transform({ input }, ({ input }) => { + return { + change_type: "transfer" as const, + order_id: input.order_id, + created_by: input.logged_in_user, + description: input.description, + internal_note: input.internal_note, + } + }) + + const change = createOrderChangeStep(orderChangeInput) + + const actionInput = transform( + { order, input, change }, + ({ order, input, change }) => [ + { + order_change_id: change.id, + order_id: input.order_id, + action: ChangeActionType.TRANSFER_CUSTOMER, + version: change.version, + reference: "customer", + reference_id: input.customer_id, + details: { + token: uid(), + original_email: order.email, + }, + }, + ] + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: actionInput, + }) + + const updateOrderChangeInput = transform( + { input, change }, + ({ input, change }) => [ + { + id: change.id, + status: OrderChangeStatus.REQUESTED, + requested_by: input.logged_in_user, + requested_at: new Date(), + }, + ] + ) + + updateOrderChangesStep(updateOrderChangeInput) + + emitEventStep({ + eventName: OrderWorkflowEvents.TRANSFER_REQUESTED, + data: { id: input.order_id }, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 1b2860ecbd7b7..6ecd48838ff44 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -24,6 +24,7 @@ export type ChangeActionType = | "SHIP_ITEM" | "WRITE_OFF_ITEM" | "REINSTATE_ITEM" + | "TRANSFER_CUSTOMER" export type OrderChangeStatus = | "confirmed" @@ -2116,7 +2117,7 @@ export interface OrderChangeDTO { /** * The type of the order change */ - change_type?: "return" | "exchange" | "claim" | "edit" + change_type?: "return" | "exchange" | "claim" | "edit" | "transfer" /** * The ID of the associated order diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 0308e3ec50689..fb07ed7edec79 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -866,6 +866,7 @@ export interface CreateOrderChangeDTO { | "exchange" | "claim" | "edit" + | "transfer" /** * The description of the order change. diff --git a/packages/core/types/src/workflow/order/accept-transfer.ts b/packages/core/types/src/workflow/order/accept-transfer.ts new file mode 100644 index 0000000000000..1423c1a794a2b --- /dev/null +++ b/packages/core/types/src/workflow/order/accept-transfer.ts @@ -0,0 +1,4 @@ +export interface AcceptOrderTransferWorkflowInput { + order_id: string + token: string +} diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index bc63385aab930..46f8034777ab3 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -15,3 +15,5 @@ export * from "./receive-return" export * from "./request-item-return" export * from "./shipping-method" export * from "./update-return" +export * from "./request-transfer" +export * from "./accept-transfer" diff --git a/packages/core/types/src/workflow/order/request-transfer.ts b/packages/core/types/src/workflow/order/request-transfer.ts new file mode 100644 index 0000000000000..f557f5f32998f --- /dev/null +++ b/packages/core/types/src/workflow/order/request-transfer.ts @@ -0,0 +1,8 @@ +export interface RequestOrderTransferWorkflowInput { + order_id: string + customer_id: string + logged_in_user: string + + description?: string + internal_note?: string +} diff --git a/packages/core/utils/src/core-flows/events.ts b/packages/core/utils/src/core-flows/events.ts index 3276ae0e80f9e..5d771039a30bd 100644 --- a/packages/core/utils/src/core-flows/events.ts +++ b/packages/core/utils/src/core-flows/events.ts @@ -25,6 +25,8 @@ export const OrderWorkflowEvents = { CLAIM_CREATED: "order.claim_created", EXCHANGE_CREATED: "order.exchange_created", + + TRANSFER_REQUESTED: "order.transfer_requested", } export const UserWorkflowEvents = { diff --git a/packages/core/utils/src/order/order-change-action.ts b/packages/core/utils/src/order/order-change-action.ts index 57f43347a1286..7f8e578f32602 100644 --- a/packages/core/utils/src/order/order-change-action.ts +++ b/packages/core/utils/src/order/order-change-action.ts @@ -14,4 +14,5 @@ export enum ChangeActionType { SHIP_ITEM = "SHIP_ITEM", WRITE_OFF_ITEM = "WRITE_OFF_ITEM", REINSTATE_ITEM = "REINSTATE_ITEM", + TRANSFER_CUSTOMER = "TRANSFER_CUSTOMER", } diff --git a/packages/medusa/src/api/admin/orders/[id]/transfer/route.ts b/packages/medusa/src/api/admin/orders/[id]/transfer/route.ts new file mode 100644 index 0000000000000..becd8da8c3ba9 --- /dev/null +++ b/packages/medusa/src/api/admin/orders/[id]/transfer/route.ts @@ -0,0 +1,39 @@ +import { requestOrderTransferWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/framework/utils" +import { AdminTransferOrderType } from "../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const variables = { id: req.params.id } + + await requestOrderTransferWorkflow(req.scope).run({ + input: { + order_id: req.params.id, + customer_id: req.validatedBody.customer_id, + logged_in_user: req.auth_context.actor_id, + description: req.validatedBody.description, + internal_note: req.validatedBody.internal_note, + }, + }) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables, + fields: req.remoteQueryConfig.fields, + }) + + const [order] = await remoteQuery(queryObject) + res.status(200).json({ order }) +} diff --git a/packages/medusa/src/api/admin/orders/middlewares.ts b/packages/medusa/src/api/admin/orders/middlewares.ts index 6e3a931b05624..ece0980b55b6b 100644 --- a/packages/medusa/src/api/admin/orders/middlewares.ts +++ b/packages/medusa/src/api/admin/orders/middlewares.ts @@ -14,6 +14,7 @@ import { AdminOrderChanges, AdminOrderCreateFulfillment, AdminOrderCreateShipment, + AdminTransferOrder, } from "./validators" export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ @@ -144,4 +145,15 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/orders/:id/transfer", + middlewares: [ + validateAndTransformBody(AdminTransferOrder), + validateAndTransformQuery( + AdminGetOrdersOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api/admin/orders/validators.ts b/packages/medusa/src/api/admin/orders/validators.ts index 49a2cfb412812..04fbd26758969 100644 --- a/packages/medusa/src/api/admin/orders/validators.ts +++ b/packages/medusa/src/api/admin/orders/validators.ts @@ -120,3 +120,10 @@ export type AdminMarkOrderFulfillmentDeliveredType = z.infer< typeof AdminMarkOrderFulfillmentDelivered > export const AdminMarkOrderFulfillmentDelivered = z.object({}) + +export type AdminTransferOrderType = z.infer +export const AdminTransferOrder = z.object({ + customer_id: z.string(), + description: z.string().optional(), + internal_note: z.string().optional(), +}) diff --git a/packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts b/packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts new file mode 100644 index 0000000000000..f46a1bc46a6c4 --- /dev/null +++ b/packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts @@ -0,0 +1,29 @@ +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/framework/types" +import { + acceptOrderTransferWorkflow, + getOrderDetailWorkflow, +} from "@medusajs/core-flows" + +import { StoreAcceptOrderTransferType } from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + await acceptOrderTransferWorkflow(req.scope).run({ + input: { + order_id: req.params.id, + token: req.validatedBody.token, + }, + }) + + const { result } = await getOrderDetailWorkflow(req.scope).run({ + input: { + fields: req.remoteQueryConfig.fields, + order_id: req.params.id, + }, + }) + + res.status(200).json({ order: result as HttpTypes.StoreOrder }) +} diff --git a/packages/medusa/src/api/store/orders/middlewares.ts b/packages/medusa/src/api/store/orders/middlewares.ts index 26ed5fa4e6a09..c30ea4f3e90ec 100644 --- a/packages/medusa/src/api/store/orders/middlewares.ts +++ b/packages/medusa/src/api/store/orders/middlewares.ts @@ -1,8 +1,15 @@ -import { MiddlewareRoute } from "@medusajs/framework/http" +import { + MiddlewareRoute, + validateAndTransformBody, +} from "@medusajs/framework/http" import { authenticate } from "../../../utils/middlewares/authenticate-middleware" import { validateAndTransformQuery } from "@medusajs/framework" import * as QueryConfig from "./query-config" -import { StoreGetOrderParams, StoreGetOrdersParams } from "./validators" +import { + StoreGetOrderParams, + StoreGetOrdersParams, + StoreAcceptOrderTransfer, +} from "./validators" export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ { @@ -26,4 +33,15 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/store/orders/:id/transfer/accept", + middlewares: [ + validateAndTransformBody(StoreAcceptOrderTransfer), + validateAndTransformQuery( + StoreGetOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api/store/orders/validators.ts b/packages/medusa/src/api/store/orders/validators.ts index a2736efc1b77c..bb60fce0c6b2b 100644 --- a/packages/medusa/src/api/store/orders/validators.ts +++ b/packages/medusa/src/api/store/orders/validators.ts @@ -18,3 +18,11 @@ export const StoreGetOrdersParams = createFindParams({ .merge(applyAndAndOrOperators(StoreGetOrdersParamsFields)) export type StoreGetOrdersParamsType = z.infer + +export const StoreAcceptOrderTransfer = z.object({ + token: z.string().min(1), +}) + +export type StoreAcceptOrderTransferType = z.infer< + typeof StoreAcceptOrderTransfer +> diff --git a/packages/modules/order/src/types/utils/index.ts b/packages/modules/order/src/types/utils/index.ts index 127d6cbb4275f..399106897f7f2 100644 --- a/packages/modules/order/src/types/utils/index.ts +++ b/packages/modules/order/src/types/utils/index.ts @@ -56,6 +56,8 @@ export type VirtualOrder = { total: BigNumberInput + customer_id?: string + transactions?: OrderTransaction[] metadata?: Record } diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 39b127cb6e850..7de6684b52a3c 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -13,3 +13,4 @@ export * from "./ship-item" export * from "./shipping-add" export * from "./shipping-remove" export * from "./write-off-item" +export * from "./transfer-customer" diff --git a/packages/modules/order/src/utils/actions/transfer-customer.ts b/packages/modules/order/src/utils/actions/transfer-customer.ts new file mode 100644 index 0000000000000..20e8bef098aad --- /dev/null +++ b/packages/modules/order/src/utils/actions/transfer-customer.ts @@ -0,0 +1,19 @@ +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { OrderChangeProcessing } from "../calculate-order-change" +import { setActionReference } from "../set-action-reference" + +OrderChangeProcessing.registerActionType(ChangeActionType.TRANSFER_CUSTOMER, { + operation({ action, currentOrder, options }) { + currentOrder.customer_id = action.reference_id + + setActionReference(currentOrder, action, options) + }, + validate({ action }) { + if (!action.reference_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference to customer ID is required" + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/apply-order-changes.ts b/packages/modules/order/src/utils/apply-order-changes.ts index 7ccdaa310c654..4d095d84b28d3 100644 --- a/packages/modules/order/src/utils/apply-order-changes.ts +++ b/packages/modules/order/src/utils/apply-order-changes.ts @@ -27,6 +27,13 @@ export function applyChangesToOrder( const summariesToUpsert: any[] = [] const orderToUpdate: any[] = [] + const orderEditableAttributes = [ + "customer_id", + "sales_channel_id", + "email", + "no_notification", + ] + const calculatedOrders = {} for (const order of orders) { const calculated = calculateOrderChange({ @@ -41,6 +48,17 @@ export function applyChangesToOrder( calculatedOrders[order.id] = calculated const version = actionsMap[order.id]?.[0]?.version ?? order.version + const orderAttributes: { + version?: number + customer_id?: string + } = {} + + // Editable attributes that have changed + for (const attr of orderEditableAttributes) { + if (order[attr] !== calculated.order[attr]) { + orderAttributes[attr] = calculated.order[attr] + } + } for (const item of calculated.order.items) { if (MathBN.lte(item.quantity, 0)) { @@ -113,12 +131,16 @@ export function applyChangesToOrder( } } + orderAttributes.version = version + } + + if (Object.keys(orderAttributes).length > 0) { orderToUpdate.push({ selector: { id: order.id, }, data: { - version, + ...orderAttributes, }, }) } From 18a60e2d2e533cbd761f420c420c1795cf7f86b9 Mon Sep 17 00:00:00 2001 From: Jim Johnston Date: Tue, 19 Nov 2024 10:52:26 +0000 Subject: [PATCH 11/14] docs: S3 providers - add example for DigitalOcean Spaces endpoint (#10145) The [S3 providers docs](https://docs.medusajs.com/resources/architectural-modules/file/s3#:R1d5rtttqj5db:) instructs DigitalOcean users to set the endpoint address to the "Spaces Origin Endpoint": ``` For DigitalOcean Spaces, it's the Spaces Origin Endpoint. ``` On DigitalOcean's user interface they include the bucket name in their origin endpoint: ![image](https://github.com/user-attachments/assets/e4c6ab05-ff1c-40ad-bb2b-49653b990e6b) Using this url for the endpoint will cause an error. The example URL `https://{region}.digitaloceanspaces.com` should tip users off to remove the bucketname. Cheers! --- www/apps/resources/app/architectural-modules/file/s3/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/apps/resources/app/architectural-modules/file/s3/page.mdx b/www/apps/resources/app/architectural-modules/file/s3/page.mdx index 9048850e23b1e..e94aa3dbaed1a 100644 --- a/www/apps/resources/app/architectural-modules/file/s3/page.mdx +++ b/www/apps/resources/app/architectural-modules/file/s3/page.mdx @@ -260,7 +260,7 @@ module.exports = { - For AWS S3, the endpoint is of the format `https://s3.{region}.amazonaws.com` - For MinIO, it's the URL to the MinIO server. - - For DigitalOcean Spaces, it's the Spaces Origin Endpoint. + - For DigitalOcean Spaces, it's the Spaces Origin Endpoint of the format `https://{region}.digitaloceanspaces.com`. - For Supabase, it's the Endpoint URL in the [Storage Settings](https://supabase.com/docs/guides/storage/s3/authentication?queryGroups=language&language=javascript#s3-access-keys). From 661ea7865ca0b04c33243e51f86946b4b6116777 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Nov 2024 16:28:33 +0530 Subject: [PATCH 12/14] feat: add optional fields (#10150) --- .changeset/beige-parents-roll.md | 6 + packages/core/types/src/dml/index.ts | 41 ++++-- .../src/dml/__tests__/array-property.spec.ts | 1 + .../src/dml/__tests__/base-property.spec.ts | 97 +++++++++++++- .../dml/__tests__/big-number-property.spec.ts | 1 + .../dml/__tests__/boolean-property.spec.ts | 1 + .../dml/__tests__/date-time-property.spec.ts | 1 + .../src/dml/__tests__/entity-builder.spec.ts | 125 ++++++++++++++++++ .../src/dml/__tests__/enum-schema.spec.ts | 4 + .../src/dml/__tests__/id-property.spec.ts | 2 + .../src/dml/__tests__/json-property.spec.ts | 2 + .../src/dml/__tests__/number-property.spec.ts | 1 + .../src/dml/__tests__/text-property.spec.ts | 2 + .../core/utils/src/dml/properties/base.ts | 25 +++- .../core/utils/src/dml/properties/index.ts | 1 + .../core/utils/src/dml/properties/nullable.ts | 23 +++- .../core/utils/src/dml/properties/optional.ts | 62 +++++++++ 17 files changed, 378 insertions(+), 17 deletions(-) create mode 100644 .changeset/beige-parents-roll.md create mode 100644 packages/core/utils/src/dml/properties/optional.ts diff --git a/.changeset/beige-parents-roll.md b/.changeset/beige-parents-roll.md new file mode 100644 index 0000000000000..17fb56a39a190 --- /dev/null +++ b/.changeset/beige-parents-roll.md @@ -0,0 +1,6 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat: add optional fields diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index e53b9196e7f16..0f3849a51dbc5 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -62,6 +62,7 @@ export type PropertyMetadata = { fieldName: string defaultValue?: any nullable: boolean + optional: boolean dataType: { name: KnownDataTypes options?: Record @@ -177,23 +178,37 @@ export type InferHasManyFields = Relation extends () => IDmlEntity< */ export type InferManyToManyFields = InferHasManyFields +/** + * Only processed property that can be undefined and mark them as optional + */ +export type InferOptionalFields = Prettify<{ + [K in keyof Schema as undefined extends Schema[K]["$dataType"] + ? K + : never]?: Schema[K]["$dataType"] +}> + /** * Inferring the types of the schema fields from the DML * entity */ -export type InferSchemaFields = Prettify<{ - [K in keyof Schema]: Schema[K] extends RelationshipType - ? Schema[K]["type"] extends "belongsTo" - ? InferBelongsToFields - : Schema[K]["type"] extends "hasOne" - ? InferHasOneFields - : Schema[K]["type"] extends "hasMany" - ? InferHasManyFields - : Schema[K]["type"] extends "manyToMany" - ? InferManyToManyFields - : never - : Schema[K]["$dataType"] -}> +export type InferSchemaFields = Prettify< + { + // Omit optional properties to manage them separately and mark them as optional + [K in keyof Schema as undefined extends Schema[K]["$dataType"] + ? never + : K]: Schema[K] extends RelationshipType + ? Schema[K]["type"] extends "belongsTo" + ? InferBelongsToFields + : Schema[K]["type"] extends "hasOne" + ? InferHasOneFields + : Schema[K]["type"] extends "hasMany" + ? InferHasManyFields + : Schema[K]["type"] extends "manyToMany" + ? InferManyToManyFields + : never + : Schema[K]["$dataType"] + } & InferOptionalFields +> /** * Helper to infer the schema type of a DmlEntity diff --git a/packages/core/utils/src/dml/__tests__/array-property.spec.ts b/packages/core/utils/src/dml/__tests__/array-property.spec.ts index b768b21156f39..78d5d5b7afbb6 100644 --- a/packages/core/utils/src/dml/__tests__/array-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/array-property.spec.ts @@ -12,6 +12,7 @@ describe("Array property", () => { name: "array", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/base-property.spec.ts b/packages/core/utils/src/dml/__tests__/base-property.spec.ts index dedb719f1622a..cfa6acef3cbd9 100644 --- a/packages/core/utils/src/dml/__tests__/base-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/base-property.spec.ts @@ -1,6 +1,6 @@ +import { PropertyMetadata } from "@medusajs/types" import { expectTypeOf } from "expect-type" import { BaseProperty } from "../properties/base" -import { PropertyMetadata } from "@medusajs/types" import { TextProperty } from "../properties/text" describe("Base property", () => { @@ -20,6 +20,7 @@ describe("Base property", () => { name: "text", }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -38,6 +39,7 @@ describe("Base property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -59,6 +61,7 @@ describe("Base property", () => { name: "text", }, nullable: true, + optional: false, indexes: [], relationships: [], }) @@ -81,6 +84,98 @@ describe("Base property", () => { }, defaultValue: "foo", nullable: false, + optional: false, + indexes: [], + relationships: [], + }) + }) + + test("apply optional modifier", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().optional() + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: false, + optional: true, + indexes: [], + relationships: [], + }) + }) + + test("apply optional and nullable modifier", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().optional().nullable() + expectTypeOf(property["$dataType"]).toEqualTypeOf< + string | undefined | null + >() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: true, + optional: true, + indexes: [], + relationships: [], + }) + }) + + test("apply nullable and optional modifier", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().nullable().optional() + expectTypeOf(property["$dataType"]).toEqualTypeOf< + string | null | undefined + >() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: true, + optional: true, + indexes: [], + relationships: [], + }) + }) + + test("define default value as a callback", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().default(() => "22") + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + defaultValue: expect.any(Function), + nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts b/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts index 09ef13aefc5cc..4861fa2cb67e8 100644 --- a/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts @@ -12,6 +12,7 @@ describe("Big Number property", () => { name: "bigNumber", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts b/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts index dc3714d08107c..545f3718fdbd2 100644 --- a/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts @@ -12,6 +12,7 @@ describe("Boolean property", () => { name: "boolean", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts b/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts index 497d059a434e3..10ebc9fa06303 100644 --- a/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts @@ -12,6 +12,7 @@ describe("DateTime property", () => { name: "dateTime", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index db304de5a9d9b..73a11a122d460 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -8,6 +8,7 @@ import { toMikroOrmEntities, toMikroORMEntity, } from "../helpers/create-mikro-orm-entity" +import { InferTypeOf } from "@medusajs/types" describe("Entity builder", () => { beforeEach(() => { @@ -1447,6 +1448,130 @@ describe("Entity builder", () => { }, }) }) + + test("define a property with default runtime value", () => { + const user = model.define("user", { + id: model.number(), + username: model.text().default((schema) => { + const { email } = schema as InferTypeOf + return email.replace(/\@.*/, "") + }), + email: model.text(), + spend_limit: model.bigNumber().default(500.4), + }) + + const User = toMikroORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: string + deleted_at: Date | null + }>() + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + + expect(metaData.filters).toEqual({ + softDeletable: { + name: "softDeletable", + cond: expect.any(Function), + default: true, + args: false, + }, + }) + + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + fieldName: "id", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + default: expect.any(Function), + columnType: "text", + name: "username", + fieldName: "username", + nullable: false, + getter: false, + setter: false, + }, + email: { + reference: "scalar", + type: "string", + columnType: "text", + name: "email", + fieldName: "email", + nullable: false, + getter: false, + setter: false, + }, + spend_limit: { + columnType: "numeric", + default: 500.4, + getter: true, + name: "spend_limit", + fieldName: "spend_limit", + nullable: false, + reference: "scalar", + setter: true, + trackChanges: false, + type: "any", + }, + raw_spend_limit: { + columnType: "jsonb", + getter: false, + name: "raw_spend_limit", + fieldName: "raw_spend_limit", + nullable: false, + reference: "scalar", + setter: false, + type: "any", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + fieldName: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + fieldName: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + fieldName: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + }) }) describe("Entity builder | id", () => { diff --git a/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts b/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts index 98cf2ad4deb57..fabcec7cf1057 100644 --- a/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts +++ b/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts @@ -17,6 +17,7 @@ describe("Enum property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -42,6 +43,7 @@ describe("Enum property", () => { }, }, nullable: true, + optional: false, indexes: [], relationships: [], }) @@ -66,6 +68,7 @@ describe("Enum property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -90,6 +93,7 @@ describe("Enum property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/id-property.spec.ts b/packages/core/utils/src/dml/__tests__/id-property.spec.ts index ce63f59da3b4f..dc6aedbad1a0c 100644 --- a/packages/core/utils/src/dml/__tests__/id-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/id-property.spec.ts @@ -13,6 +13,7 @@ describe("Id property", () => { options: {}, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -29,6 +30,7 @@ describe("Id property", () => { options: {}, }, nullable: false, + optional: false, indexes: [], relationships: [], primaryKey: true, diff --git a/packages/core/utils/src/dml/__tests__/json-property.spec.ts b/packages/core/utils/src/dml/__tests__/json-property.spec.ts index 887c615276afa..d965ddf34a298 100644 --- a/packages/core/utils/src/dml/__tests__/json-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/json-property.spec.ts @@ -12,6 +12,7 @@ describe("JSON property", () => { name: "json", }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -30,6 +31,7 @@ describe("JSON property", () => { a: 1, }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/number-property.spec.ts b/packages/core/utils/src/dml/__tests__/number-property.spec.ts index 6d4231f3125b3..6f5e8b7bdd0aa 100644 --- a/packages/core/utils/src/dml/__tests__/number-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/number-property.spec.ts @@ -13,6 +13,7 @@ describe("Number property", () => { options: {}, }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/text-property.spec.ts b/packages/core/utils/src/dml/__tests__/text-property.spec.ts index b14c2eca7e0b8..f30bf9f60296e 100644 --- a/packages/core/utils/src/dml/__tests__/text-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/text-property.spec.ts @@ -13,6 +13,7 @@ describe("Text property", () => { options: { searchable: false }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -29,6 +30,7 @@ describe("Text property", () => { options: { searchable: false }, }, nullable: false, + optional: false, indexes: [], relationships: [], primaryKey: true, diff --git a/packages/core/utils/src/dml/properties/base.ts b/packages/core/utils/src/dml/properties/base.ts index 2ab719c4a1c25..e18bcba22ecd6 100644 --- a/packages/core/utils/src/dml/properties/base.ts +++ b/packages/core/utils/src/dml/properties/base.ts @@ -1,5 +1,6 @@ import { PropertyMetadata, PropertyType } from "@medusajs/types" import { NullableModifier } from "./nullable" +import { OptionalModifier } from "./optional" /** * The BaseProperty class offers shared affordances to define @@ -15,7 +16,7 @@ export abstract class BaseProperty implements PropertyType { /** * Default value for the property */ - #defaultValue?: T + #defaultValue?: T | ((schema: any) => T) /** * The runtime dataType for the schema. It is not the same as @@ -48,6 +49,25 @@ export abstract class BaseProperty implements PropertyType { return new NullableModifier(this) } + /** + * This method indicates that a property's value can be `optional`. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * price: model.bigNumber().optional(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + optional() { + return new OptionalModifier(this) + } + /** * This method defines an index on a property. * @@ -119,7 +139,7 @@ export abstract class BaseProperty implements PropertyType { * * @customNamespace Property Configuration Methods */ - default(value: T) { + default(value: T | ((schema: any) => T)) { this.#defaultValue = value return this } @@ -132,6 +152,7 @@ export abstract class BaseProperty implements PropertyType { fieldName, dataType: this.dataType, nullable: false, + optional: false, defaultValue: this.#defaultValue, indexes: this.#indexes, relationships: this.#relationships, diff --git a/packages/core/utils/src/dml/properties/index.ts b/packages/core/utils/src/dml/properties/index.ts index 9c55e703660a0..19c597fa23d65 100644 --- a/packages/core/utils/src/dml/properties/index.ts +++ b/packages/core/utils/src/dml/properties/index.ts @@ -7,6 +7,7 @@ export * from "./enum" export * from "./id" export * from "./json" export * from "./nullable" +export * from "./optional" export * from "./number" export * from "./primary-key" export * from "./text" diff --git a/packages/core/utils/src/dml/properties/nullable.ts b/packages/core/utils/src/dml/properties/nullable.ts index 685addae025a6..238bde9c59b90 100644 --- a/packages/core/utils/src/dml/properties/nullable.ts +++ b/packages/core/utils/src/dml/properties/nullable.ts @@ -1,10 +1,11 @@ import { PropertyType } from "@medusajs/types" +import { OptionalModifier } from "./optional" const IsNullableModifier = Symbol.for("isNullableModifier") /** * Nullable modifier marks a schema node as nullable */ -export class NullableModifier> +export class NullableModifier>> implements PropertyType { [IsNullableModifier]: true = true @@ -12,6 +13,7 @@ export class NullableModifier> static isNullableModifier(obj: any): obj is NullableModifier { return !!obj?.[IsNullableModifier] } + /** * A type-only property to infer the JavScript data-type * of the schema property @@ -28,6 +30,25 @@ export class NullableModifier> this.#schema = schema } + /** + * This method indicates that a property's value can be `optional`. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * price: model.bigNumber().optional(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + optional() { + return new OptionalModifier(this) + } + /** * Returns the serialized metadata */ diff --git a/packages/core/utils/src/dml/properties/optional.ts b/packages/core/utils/src/dml/properties/optional.ts new file mode 100644 index 0000000000000..8c514c1900922 --- /dev/null +++ b/packages/core/utils/src/dml/properties/optional.ts @@ -0,0 +1,62 @@ +import { PropertyType } from "@medusajs/types" +import { NullableModifier } from "./nullable" + +const IsOptionalModifier = Symbol.for("IsOptionalModifier") + +/** + * Nullable modifier marks a schema node as optional and + * allows for default values + */ +export class OptionalModifier> + implements PropertyType +{ + [IsOptionalModifier]: true = true + + static isOptionalModifier(obj: any): obj is OptionalModifier { + return !!obj?.[IsOptionalModifier] + } + + /** + * A type-only property to infer the JavScript data-type + * of the schema property + */ + declare $dataType: T | undefined + + /** + * The parent schema on which the nullable modifier is + * applied + */ + #schema: Schema + + constructor(schema: Schema) { + this.#schema = schema + } + + /** + * This method indicates that a property's value can be `null`. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * price: model.bigNumber().optional().nullable(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + nullable() { + return new NullableModifier(this) + } + + /** + * Returns the serialized metadata + */ + parse(fieldName: string) { + const schema = this.#schema.parse(fieldName) + schema.optional = true + return schema + } +} From 7aa990795cf262f98b86adb7f14d6f146620bc1d Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 19 Nov 2024 12:19:19 +0100 Subject: [PATCH 13/14] chore(): Update module provider retrieval error message and type (#10138) Partially RESOLVES FRMW-2802 **What** Improve error message and change the error type when retrieving a provider from a local container fail --- .changeset/wet-ears-wonder.md | 9 ++++++++ .../auth-module-service/index.spec.ts | 3 ++- .../modules/auth/src/services/auth-module.ts | 2 ++ .../auth/src/services/auth-provider.ts | 17 ++++++++++----- .../services/fulfillment-module-service.ts | 2 ++ .../src/services/fulfillment-provider.ts | 16 ++++++++++---- .../locking/src/services/locking-module.ts | 2 ++ .../locking/src/services/locking-provider.ts | 21 ++++++++++++++----- .../services/notification-module-service.ts | 2 ++ .../src/services/notification-provider.ts | 21 ++++++++++++++----- .../payment/src/services/payment-module.ts | 2 ++ .../payment/src/services/payment-provider.ts | 21 +++++++++++++++---- 12 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 .changeset/wet-ears-wonder.md diff --git a/.changeset/wet-ears-wonder.md b/.changeset/wet-ears-wonder.md new file mode 100644 index 0000000000000..59ecc1d364eb9 --- /dev/null +++ b/.changeset/wet-ears-wonder.md @@ -0,0 +1,9 @@ +--- +"@medusajs/auth": patch +"@medusajs/fulfillment": patch +"@medusajs/locking": patch +"@medusajs/notification": patch +"@medusajs/payment": patch +--- + +chore(): Update module provider retrieval error message and type diff --git a/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts b/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts index 26f4759ace9b5..5f53ed9f1279f 100644 --- a/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts +++ b/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts @@ -84,7 +84,8 @@ moduleIntegrationTestRunner({ expect(err).toEqual({ success: false, - error: "Could not find a auth provider with id: facebook", + error: + "\n Unable to retrieve the auth provider with id: facebook\n Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.\n ", }) }) diff --git a/packages/modules/auth/src/services/auth-module.ts b/packages/modules/auth/src/services/auth-module.ts index 1c1b653b4dd75..6a314989e3ec8 100644 --- a/packages/modules/auth/src/services/auth-module.ts +++ b/packages/modules/auth/src/services/auth-module.ts @@ -6,6 +6,7 @@ import { Context, DAL, InternalModuleDeclaration, + Logger, ModuleJoinerConfig, ModulesSdkTypes, } from "@medusajs/framework/types" @@ -24,6 +25,7 @@ type InjectedDependencies = { authIdentityService: ModulesSdkTypes.IMedusaInternalService providerIdentityService: ModulesSdkTypes.IMedusaInternalService authProviderService: AuthProviderService + logger?: Logger } export default class AuthModuleService extends MedusaService<{ diff --git a/packages/modules/auth/src/services/auth-provider.ts b/packages/modules/auth/src/services/auth-provider.ts index 3c8e39f22085f..193d9f412f7c5 100644 --- a/packages/modules/auth/src/services/auth-provider.ts +++ b/packages/modules/auth/src/services/auth-provider.ts @@ -3,21 +3,26 @@ import { AuthenticationResponse, AuthIdentityProviderService, AuthTypes, + Logger, } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" import { AuthProviderRegistrationPrefix } from "@types" type InjectedDependencies = { [ key: `${typeof AuthProviderRegistrationPrefix}${string}` ]: AuthTypes.IAuthProvider + logger?: Logger } export default class AuthProviderService { protected dependencies: InjectedDependencies + #logger: Logger constructor(container: InjectedDependencies) { this.dependencies = container + this.#logger = container["logger"] + ? container.logger + : (console as unknown as Logger) } protected retrieveProviderRegistration( @@ -26,10 +31,12 @@ export default class AuthProviderService { try { return this.dependencies[`${AuthProviderRegistrationPrefix}${providerId}`] } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a auth provider with id: ${providerId}` - ) + const errMessage = ` + Unable to retrieve the auth provider with id: ${providerId} + Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file. + ` + this.#logger.error(errMessage) + throw new Error(errMessage) } } diff --git a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts index 5fe4f9ecfd749..486ecf19e663b 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts @@ -7,6 +7,7 @@ import { FulfillmentTypes, IFulfillmentModuleService, InternalModuleDeclaration, + Logger, ModuleJoinerConfig, ModulesSdkTypes, ShippingOptionDTO, @@ -76,6 +77,7 @@ type InjectedDependencies = { shippingOptionTypeService: ModulesSdkTypes.IMedusaInternalService fulfillmentProviderService: FulfillmentProviderService fulfillmentService: ModulesSdkTypes.IMedusaInternalService + logger?: Logger } export default class FulfillmentModuleService diff --git a/packages/modules/fulfillment/src/services/fulfillment-provider.ts b/packages/modules/fulfillment/src/services/fulfillment-provider.ts index 75b4c522fb069..ca926f66d58dd 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-provider.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-provider.ts @@ -3,6 +3,7 @@ import { DAL, FulfillmentTypes, IFulfillmentProvider, + Logger, } from "@medusajs/framework/types" import { MedusaError, @@ -12,6 +13,7 @@ import { import { FulfillmentProvider } from "@models" type InjectedDependencies = { + logger?: Logger fulfillmentProviderRepository: DAL.RepositoryService [key: `fp_${string}`]: FulfillmentTypes.IFulfillmentProvider } @@ -22,11 +24,15 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.MedusaIn FulfillmentProvider ) { protected readonly fulfillmentProviderRepository_: DAL.RepositoryService + #logger: Logger constructor(container: InjectedDependencies) { super(container) this.fulfillmentProviderRepository_ = container.fulfillmentProviderRepository + this.#logger = container["logger"] + ? container.logger + : (console as unknown as Logger) } static getRegistrationIdentifier( @@ -48,10 +54,12 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.MedusaIn try { return this.__container__[`fp_${providerId}`] } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a fulfillment provider with id: ${providerId}` - ) + const errMessage = ` + Unable to retrieve the fulfillment provider with id: ${providerId} + Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file. + ` + this.#logger.error(errMessage) + throw new Error(errMessage) } } diff --git a/packages/modules/locking/src/services/locking-module.ts b/packages/modules/locking/src/services/locking-module.ts index 6945da85495e0..fd55a58347965 100644 --- a/packages/modules/locking/src/services/locking-module.ts +++ b/packages/modules/locking/src/services/locking-module.ts @@ -2,6 +2,7 @@ import { Context, ILockingModule, InternalModuleDeclaration, + Logger, } from "@medusajs/types" import { EntityManager } from "@mikro-orm/core" import { LockingDefaultProvider } from "@types" @@ -10,6 +11,7 @@ import LockingProviderService from "./locking-provider" type InjectedDependencies = { manager: EntityManager lockingProviderService: LockingProviderService + logger?: Logger [LockingDefaultProvider]: string } diff --git a/packages/modules/locking/src/services/locking-provider.ts b/packages/modules/locking/src/services/locking-provider.ts index eca5769897b27..d876a51ffe792 100644 --- a/packages/modules/locking/src/services/locking-provider.ts +++ b/packages/modules/locking/src/services/locking-provider.ts @@ -1,16 +1,25 @@ -import { Constructor, ILockingProvider } from "@medusajs/framework/types" +import { + Constructor, + ILockingProvider, + Logger, +} from "@medusajs/framework/types" import { MedusaError } from "@medusajs/framework/utils" import { LockingProviderRegistrationPrefix } from "../types" type InjectedDependencies = { [key: `lp_${string}`]: ILockingProvider + logger?: Logger } export default class LockingProviderService { protected __container__: InjectedDependencies + #logger: Logger constructor(container: InjectedDependencies) { this.__container__ = container + this.#logger = container["logger"] + ? container.logger + : (console as unknown as Logger) } static getRegistrationIdentifier( @@ -31,10 +40,12 @@ export default class LockingProviderService { `${LockingProviderRegistrationPrefix}${providerId}` ] } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a locking provider with id: ${providerId}` - ) + const errMessage = ` + Unable to retrieve the locking provider with id: ${providerId} + Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file. + ` + this.#logger.error(errMessage) + throw new Error(errMessage) } } } diff --git a/packages/modules/notification/src/services/notification-module-service.ts b/packages/modules/notification/src/services/notification-module-service.ts index f40550559351b..75ee656da70ee 100644 --- a/packages/modules/notification/src/services/notification-module-service.ts +++ b/packages/modules/notification/src/services/notification-module-service.ts @@ -4,6 +4,7 @@ import { InferEntityType, INotificationModuleService, InternalModuleDeclaration, + Logger, ModulesSdkTypes, NotificationTypes, } from "@medusajs/framework/types" @@ -22,6 +23,7 @@ import { eventBuilders } from "@utils" import NotificationProviderService from "./notification-provider" type InjectedDependencies = { + logger?: Logger baseRepository: DAL.RepositoryService notificationService: ModulesSdkTypes.IMedusaInternalService< typeof Notification diff --git a/packages/modules/notification/src/services/notification-provider.ts b/packages/modules/notification/src/services/notification-provider.ts index 9b08b8b74cb35..adb07b7904a5d 100644 --- a/packages/modules/notification/src/services/notification-provider.ts +++ b/packages/modules/notification/src/services/notification-provider.ts @@ -1,13 +1,15 @@ import { DAL, InferEntityType, + Logger, NotificationTypes, } from "@medusajs/framework/types" -import { MedusaError, ModulesSdkUtils } from "@medusajs/framework/utils" +import { ModulesSdkUtils } from "@medusajs/framework/utils" import { NotificationProvider } from "@models" import { NotificationProviderRegistrationPrefix } from "@types" type InjectedDependencies = { + logger?: Logger notificationProviderRepository: DAL.RepositoryService< InferEntityType > @@ -25,7 +27,11 @@ export default class NotificationProviderService extends ModulesSdkUtils.MedusaI protected readonly notificationProviderRepository_: DAL.RepositoryService< InferEntityType > + // We can store the providers in a memory since they can only be registered on startup and not changed during runtime + + #logger: Logger + protected providersCache: Map< string, InferEntityType @@ -35,6 +41,9 @@ export default class NotificationProviderService extends ModulesSdkUtils.MedusaI super(container) this.notificationProviderRepository_ = container.notificationProviderRepository + this.#logger = container["logger"] + ? container.logger + : (console as unknown as Logger) } protected retrieveProviderRegistration( @@ -45,10 +54,12 @@ export default class NotificationProviderService extends ModulesSdkUtils.MedusaI `${NotificationProviderRegistrationPrefix}${providerId}` ] } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a notification provider with id: ${providerId}` - ) + const errMessage = ` + Unable to retrieve the notification provider with id: ${providerId} + Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file. + ` + this.#logger.error(errMessage) + throw new Error(errMessage) } } diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index bc24d4ea7d6d0..fdef4b55afc11 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -13,6 +13,7 @@ import { FindConfig, InternalModuleDeclaration, IPaymentModuleService, + Logger, ModuleJoinerConfig, ModulesSdkTypes, PaymentCollectionDTO, @@ -54,6 +55,7 @@ import { joinerConfig } from "../joiner-config" import PaymentProviderService from "./payment-provider" type InjectedDependencies = { + logger?: Logger baseRepository: DAL.RepositoryService paymentService: ModulesSdkTypes.IMedusaInternalService captureService: ModulesSdkTypes.IMedusaInternalService diff --git a/packages/modules/payment/src/services/payment-provider.ts b/packages/modules/payment/src/services/payment-provider.ts index 919689525d6fa..3fbccb504176d 100644 --- a/packages/modules/payment/src/services/payment-provider.ts +++ b/packages/modules/payment/src/services/payment-provider.ts @@ -3,6 +3,7 @@ import { CreatePaymentProviderSession, DAL, IPaymentProvider, + Logger, PaymentProviderAuthorizeResponse, PaymentProviderDataInput, PaymentProviderError, @@ -21,6 +22,7 @@ import { PaymentProvider } from "@models" import { EOL } from "os" type InjectedDependencies = { + logger?: Logger paymentProviderRepository: DAL.RepositoryService [key: `pp_${string}`]: IPaymentProvider } @@ -28,14 +30,25 @@ type InjectedDependencies = { export default class PaymentProviderService extends ModulesSdkUtils.MedusaInternalService( PaymentProvider ) { + #logger: Logger + + constructor(container: InjectedDependencies) { + super(container) + this.#logger = container["logger"] + ? container.logger + : (console as unknown as Logger) + } + retrieveProvider(providerId: string): IPaymentProvider { try { return this.__container__[providerId] as IPaymentProvider } catch (e) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a payment provider with id: ${providerId}` - ) + const errMessage = ` + Unable to retrieve the payment provider with id: ${providerId} + Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file. + ` + this.#logger.error(errMessage) + throw new Error(errMessage) } } From 39e81d8d218b8ec4fd480368cd819845c6bade54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:22:55 +0100 Subject: [PATCH 14/14] fix(dashboard): order edit - display item quantity change correctly (#10078) **What** - use a diff form change action details to display edit history --- .../order-activity-section/order-timeline.tsx | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx index d4c4c6162bc3d..15741b7c29992 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx @@ -376,9 +376,7 @@ const useActivityItems = (order: AdminOrder): Activity[] => { : edit.status === "canceled" ? edit.canceled_at : edit.created_at, - children: isConfirmed ? ( - - ) : null, + children: isConfirmed ? : null, }) } @@ -839,18 +837,12 @@ const ExchangeBody = ({ ) } -const OrderEditBody = ({ - edit, - itemsMap, -}: { - edit: AdminOrderChange - itemsMap: Map -}) => { +const OrderEditBody = ({ edit }: { edit: AdminOrderChange }) => { const { t } = useTranslation() const [itemsAdded, itemsRemoved] = useMemo( - () => countItemsChange(edit.actions, itemsMap), - [edit, itemsMap] + () => countItemsChange(edit.actions), + [edit] ) return ( @@ -873,10 +865,7 @@ const OrderEditBody = ({ /** * Returns count of added and removed item quantity */ -function countItemsChange( - actions: AdminOrderChange["actions"], - itemsMap: Map -) { +function countItemsChange(actions: AdminOrderChange["actions"]) { let added = 0 let removed = 0 @@ -885,20 +874,12 @@ function countItemsChange( added += action.details!.quantity as number } if (action.action === "ITEM_UPDATE") { - const newQuantity = action.details!.quantity as number - const originalQuantity: number | undefined = itemsMap.get( - action.details!.reference_id as string - )?.quantity - - if (typeof originalQuantity === "number") { - const diff = Math.abs(newQuantity - originalQuantity) - - if (newQuantity > originalQuantity) { - added += diff - } - if (newQuantity < originalQuantity) { - removed += diff - } + const quantityDiff = action.details!.quantity_diff as number + + if (quantityDiff > 0) { + added += quantityDiff + } else { + removed += Math.abs(quantityDiff) } } })