@rahul_vendure/vendure-plugin-channel-product-availability 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +193 -2
- package/dist/api/admin.resolver.d.ts +18 -0
- package/dist/api/admin.resolver.js +118 -0
- package/dist/api/api-extensions.d.ts +1 -0
- package/dist/api/api-extensions.js +87 -0
- package/dist/api/order.resolver.d.ts +25 -0
- package/dist/api/order.resolver.js +96 -0
- package/dist/api/shop-catalog.resolver.d.ts +34 -0
- package/dist/api/shop-catalog.resolver.js +146 -0
- package/dist/channel-product-availability.plugin.d.ts +26 -0
- package/dist/channel-product-availability.plugin.js +143 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +6 -0
- package/dist/entities/product-channel-availability.entity.d.ts +15 -0
- package/dist/entities/product-channel-availability.entity.js +52 -0
- package/dist/events/product-channel-availability.event.d.ts +34 -0
- package/dist/events/product-channel-availability.event.js +30 -0
- package/dist/generated-graphql-types.d.ts +157 -0
- package/dist/generated-graphql-types.js +18 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +18 -0
- package/dist/migrations/channel-product-availability.migration.d.ts +27 -0
- package/dist/migrations/channel-product-availability.migration.js +49 -0
- package/dist/order/channel-availability-order-interceptor.d.ts +18 -0
- package/dist/order/channel-availability-order-interceptor.js +53 -0
- package/dist/order/channel-availability-order-process.d.ts +10 -0
- package/dist/order/channel-availability-order-process.js +49 -0
- package/dist/order/override-purchase.service.d.ts +48 -0
- package/dist/order/override-purchase.service.js +167 -0
- package/dist/search/search-index-availability.subscriber.d.ts +24 -0
- package/dist/search/search-index-availability.subscriber.js +96 -0
- package/dist/search/search-index-item-metadata.d.ts +9 -0
- package/dist/search/search-index-item-metadata.js +14 -0
- package/dist/search/search-index-sync.service.d.ts +23 -0
- package/dist/search/search-index-sync.service.js +89 -0
- package/dist/services/availability-rules.d.ts +34 -0
- package/dist/services/availability-rules.js +49 -0
- package/dist/services/channel-access.service.d.ts +15 -0
- package/dist/services/channel-access.service.js +46 -0
- package/dist/services/channel-product-availability.service.d.ts +119 -0
- package/dist/services/channel-product-availability.service.js +280 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +10 -0
- package/package.json +57 -4
- package/src/dashboard/channel-availability-block.tsx +170 -0
- package/src/dashboard/channel-availability.graphql.ts +48 -0
- package/src/dashboard/index.tsx +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `ProductChannelAvailability` entity: sparse per-(product, channel) overrides of `Product.enabled`, with a unique key, a lookup index and cascading foreign keys.
|
|
10
|
+
- `ChannelProductAvailabilityMigration1792195200000`: an opt-in migration built from the entity metadata, so it works on any supported database.
|
|
11
|
+
- `ChannelProductAvailabilityService`: the single source of truth for the rule. Reads are batched and cached per request; writes are idempotent and concurrency-safe.
|
|
12
|
+
- Shop API enforcement for `products`, `product` and `Collection.productVariants`, applied in SQL so counts and pagination stay correct.
|
|
13
|
+
- DefaultSearchPlugin index sync: overrides are applied when index rows are written and patched immediately when an override changes.
|
|
14
|
+
- Purchasing of products that are globally disabled but enabled in a channel, via the shop `addItemToOrder` / `addItemsToOrder` and admin `addItemToDraftOrder` mutations.
|
|
15
|
+
- `OrderInterceptor` that rejects adding, or increasing the quantity of, explicitly disabled products.
|
|
16
|
+
- `OrderProcess` that blocks checkout while the cart still contains explicitly disabled products.
|
|
17
|
+
- Admin API queries and mutations, with permission checks in the target channel.
|
|
18
|
+
- `ProductChannelAvailabilityEvent`, plus clean-up of overrides when a product is deleted, a product is removed from a channel, or a channel is deleted.
|
|
19
|
+
- React Dashboard block on the product detail page.
|
|
20
|
+
- Tested on Vendure 3.6.1 and 3.8.0, each on Postgres and SQLite.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Yadav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,194 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rahul_vendure/vendure-plugin-channel-product-availability
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Per-channel product availability for [Vendure](https://vendure.io). Enable a product in one channel and disable it in another, without touching the global `Product.enabled` flag. The rule is enforced in catalog queries, search, cart and checkout.
|
|
4
|
+
|
|
5
|
+
| Store | Global `Product.enabled` | Override | Customers there… |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| Nepal | `true` | — | can find and buy it |
|
|
8
|
+
| India | `true` | `false` | cannot find or buy it |
|
|
9
|
+
| UK | `false` | `true` | can find and buy it |
|
|
10
|
+
|
|
11
|
+
- **Explicit overrides always win.** No override means the global flag applies, exactly as in unmodified Vendure.
|
|
12
|
+
- **Overrides are sparse.** No rows are created up front and nothing is back-filled.
|
|
13
|
+
- **Native rules still apply.** An override never assigns a product to a channel and never bypasses variant `enabled`, stock, prices or channel membership.
|
|
14
|
+
- **Standalone.** No other custom plugin is needed, and Vendure core is not modified.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @rahul_vendure/vendure-plugin-channel-product-availability
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { ChannelProductAvailabilityPlugin } from '@rahul_vendure/vendure-plugin-channel-product-availability';
|
|
24
|
+
|
|
25
|
+
export const config: VendureConfig = {
|
|
26
|
+
plugins: [
|
|
27
|
+
DefaultSearchPlugin.init({}),
|
|
28
|
+
ChannelProductAvailabilityPlugin.init({}),
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Database migration
|
|
34
|
+
|
|
35
|
+
The plugin adds one table, `product_channel_availability`. If your project uses `synchronize: false`, pick **one** of the following:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// a) use the migration that ships with the plugin (works on Postgres, MySQL/MariaDB and SQLite,
|
|
39
|
+
// and with integer or UUID ids)
|
|
40
|
+
import { ChannelProductAvailabilityMigration1792195200000 } from '@rahul_vendure/vendure-plugin-channel-product-availability';
|
|
41
|
+
dbConnectionOptions: { migrations: [ChannelProductAvailabilityMigration1792195200000, /* yours */] }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# b) generate your own
|
|
46
|
+
npx vendure migrate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Column | Notes |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `id`, `createdAt`, `updatedAt` | standard Vendure entity columns |
|
|
52
|
+
| `productId` | FK → `product.id`, `ON DELETE CASCADE` |
|
|
53
|
+
| `channelId` | FK → `channel.id`, `ON DELETE CASCADE` |
|
|
54
|
+
| `enabled` | the override value |
|
|
55
|
+
|
|
56
|
+
Indexes: `UNIQUE (productId, channelId)` and `(channelId, enabled)`.
|
|
57
|
+
|
|
58
|
+
### Dashboard
|
|
59
|
+
|
|
60
|
+
The React Dashboard extension is discovered automatically through the plugin's `dashboard` metadata. It adds a **Channel availability** block to the product detail page. For each channel the product is assigned to, the block shows:
|
|
61
|
+
|
|
62
|
+
- whether the product is available there
|
|
63
|
+
- whether that comes from the global flag or from an override
|
|
64
|
+
- buttons to enable, disable, or go back to the global flag (**Use global**)
|
|
65
|
+
|
|
66
|
+
The block also warns when a product is disabled globally but explicitly enabled somewhere. Channels where the admin can't update show as read-only.
|
|
67
|
+
|
|
68
|
+
The backend works without the Dashboard.
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
All options default to `true`.
|
|
73
|
+
|
|
74
|
+
| Option | Effect when `true` |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `enforceCatalog` | Shop API `products`, `product` and `Collection.productVariants` follow the channel rule |
|
|
77
|
+
| `enableOverridePurchasing` | Products that are globally disabled but enabled in the channel can be added to carts (shop) and draft orders (admin) |
|
|
78
|
+
| `syncDefaultSearchIndex` | Keeps the DefaultSearchPlugin index in sync with overrides |
|
|
79
|
+
| `validateOnCheckout` | Blocks `ArrangingPayment` while the cart contains a product explicitly disabled in that channel |
|
|
80
|
+
|
|
81
|
+
Explicitly disabled products are always rejected when adding to a cart, whatever the options say.
|
|
82
|
+
|
|
83
|
+
## Admin API
|
|
84
|
+
|
|
85
|
+
```graphql
|
|
86
|
+
query {
|
|
87
|
+
productChannelAvailability(productId: "1", channelId: "2") { # channelId defaults to the active channel
|
|
88
|
+
channelCode globalEnabled override overrideState effectiveEnabled productInChannel
|
|
89
|
+
}
|
|
90
|
+
productChannelAvailabilities(productId: "1") { channelCode overrideState effectiveEnabled }
|
|
91
|
+
productChannelAvailabilityOverrides(options: { channelId: "2", enabled: false }) {
|
|
92
|
+
items { productId enabled } totalItems
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mutation {
|
|
97
|
+
setProductChannelAvailability(input: { productId: "1", channelId: "2", enabled: true }) { effectiveEnabled }
|
|
98
|
+
clearProductChannelAvailability(input: { productId: "1", channelId: "2" }) { overrideState } # → INHERITED
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Permissions.** Reads need `ReadCatalog` or `ReadProduct`; writes need `UpdateCatalog` or `UpdateProduct`. The check is made in the **target** channel, not only the active one, so an admin of channel A can't change channel B by passing its `channelId`.
|
|
103
|
+
|
|
104
|
+
**Validation.** Setting an override on a product that isn't assigned to the channel fails with a clear error. The plugin never assigns products to channels.
|
|
105
|
+
|
|
106
|
+
## Using the service
|
|
107
|
+
|
|
108
|
+
`ChannelProductAvailabilityService` is the single source of truth for the rule, and it's exported for your own code:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
constructor(private availability: ChannelProductAvailabilityService) {}
|
|
112
|
+
|
|
113
|
+
await this.availability.isEnabled(ctx, productId); // active channel
|
|
114
|
+
await this.availability.getAvailability(ctx, productId); // full detail
|
|
115
|
+
await this.availability.getAvailabilityForChannel(ctx, productId, channelId);
|
|
116
|
+
await this.availability.getOverrides(ctx, productIds); // batched, Map<productId, boolean>
|
|
117
|
+
await this.availability.setOverride(ctx, productId, true, channelId);
|
|
118
|
+
await this.availability.clearOverride(ctx, productId, channelId);
|
|
119
|
+
await this.availability.getEffectiveEnabledProductFilter(ctx); // ListQueryBuilder filter for custom queries
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Behaviour:
|
|
123
|
+
|
|
124
|
+
- Writes run in the caller's transaction and are idempotent: writing the current value is a no-op.
|
|
125
|
+
- Concurrent writers can't create duplicate rows; the unique key plus an upsert prevents it.
|
|
126
|
+
- Reads are batched and cached for the rest of the request.
|
|
127
|
+
|
|
128
|
+
### Events
|
|
129
|
+
|
|
130
|
+
The plugin publishes `ProductChannelAvailabilityEvent` after the transaction commits. Its `type` is one of:
|
|
131
|
+
|
|
132
|
+
| Type | When |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `set` | an override was created or changed |
|
|
135
|
+
| `cleared` | an override was removed (the product inherits the global flag again) |
|
|
136
|
+
| `removed` | an override was cleaned up because the product was deleted, the product was removed from the channel, or the channel was deleted |
|
|
137
|
+
| `global-changed` | `Product.enabled` changed; `channelId` is `null` |
|
|
138
|
+
|
|
139
|
+
## Search integration contract
|
|
140
|
+
|
|
141
|
+
**DefaultSearchPlugin** is supported out of the box. Its index has one row per (variant, channel, language), and the plugin keeps the `enabled` column correct in two ways:
|
|
142
|
+
|
|
143
|
+
- when you change an override, it patches the existing rows immediately in the same transaction
|
|
144
|
+
- whenever the indexer writes rows, it corrects them as they're written
|
|
145
|
+
|
|
146
|
+
As a result, results, `totalItems`, pagination and facet counts are all correct.
|
|
147
|
+
|
|
148
|
+
**Other search plugins** (Elasticsearch, Meilisearch, Typesense, etc.) need to do three things:
|
|
149
|
+
|
|
150
|
+
1. Index availability **per channel**. For a variant in channel C:
|
|
151
|
+
```ts
|
|
152
|
+
const overrides = await availabilityService.getOverrides(ctx /* channel C */, productIds);
|
|
153
|
+
const productEnabled = resolveEffectiveEnabled(product.enabled, overrides.get(String(product.id)));
|
|
154
|
+
const indexedEnabled = productEnabled && variant.enabled;
|
|
155
|
+
```
|
|
156
|
+
2. Reindex `productId` when `ProductChannelAvailabilityEvent` fires. Reindex it in `channelId`, or in every channel when `channelId` is `null`.
|
|
157
|
+
3. Filter on the indexed per-channel flag **in the search engine query**. Filtering results after pagination gives wrong counts.
|
|
158
|
+
|
|
159
|
+
## Limitations
|
|
160
|
+
|
|
161
|
+
- **Guest-cart merge at login.** Vendure's `OrderService.mergeOrders` calls core `addItemsToOrder` directly, and plugins can't override that. Suppose a customer adds a product that is *globally disabled but enabled in this channel* to a guest cart, then logs in while they already have an active cart. Core's merge rejects that line, the same way core treats any globally disabled product. Products that are globally enabled are not affected.
|
|
162
|
+
- **Changes during payment.** A product disabled *after* the order reached `ArrangingPayment` is not blocked at payment time; core does the same for globally disabled products.
|
|
163
|
+
- **Admin `modifyOrder`.** Core doesn't check `enabled` when an admin modifies a placed order, and the plugin keeps that behaviour.
|
|
164
|
+
- **Resolver overrides.** The plugin overrides these Shop API resolvers, using the override mechanism documented by Vendure: `products`, `product`, `Collection.productVariants`, `addItemToOrder` and `addItemsToOrder`. It also overrides the Admin API `addItemToDraftOrder`. Another plugin overriding the same fields would conflict. In that case, turn off `enforceCatalog` and/or `enableOverridePurchasing` and call `ChannelProductAvailabilityService` from your own resolver.
|
|
165
|
+
- **Large override sets.** Product lists filter with `id IN (...)` on the overridden ids for the channel, which is fine for thousands of overrides per channel. Overrides are meant to be exceptions, not the norm.
|
|
166
|
+
- **Interceptors called twice.** When an override-enabled product (globally disabled) is added, third-party `OrderInterceptor`s receive both `willAddItemToOrder` and `willAdjustOrderLine`.
|
|
167
|
+
|
|
168
|
+
## Compatibility
|
|
169
|
+
|
|
170
|
+
| Vendure | Database | Status |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| 3.8.0 | Postgres 14+ | ✅ 66/66 tests pass |
|
|
173
|
+
| 3.8.0 | SQLite (sql.js) | ✅ 66/66 tests pass |
|
|
174
|
+
| 3.6.1 | Postgres 14+ | ✅ 66/66 tests pass |
|
|
175
|
+
| 3.6.1 | SQLite (sql.js) | ✅ 66/66 tests pass |
|
|
176
|
+
| 3.7.x | — | expected to work, not tested |
|
|
177
|
+
| MySQL / MariaDB | — | migration is database-agnostic; not tested |
|
|
178
|
+
|
|
179
|
+
Peer dependency: `@vendure/core ^3.6.0`. Run the suite against any release with `./scripts/test-against-vendure.sh <version>`.
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npm run build # codegen + compile to dist/
|
|
185
|
+
npm test # unit + e2e (sql.js)
|
|
186
|
+
npm run test:e2e:postgres # e2e on a real Postgres (PGHOST/PGUSER/PGPASSWORD)
|
|
187
|
+
./scripts/test-against-vendure.sh 3.8.0
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for every core code path involved and the extension point used for each.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RequestContext, TransactionalConnection } from '@vendure/core';
|
|
2
|
+
import { MutationClearProductChannelAvailabilityArgs, MutationSetProductChannelAvailabilityArgs, QueryProductChannelAvailabilitiesArgs, QueryProductChannelAvailabilityArgs, QueryProductChannelAvailabilityOverridesArgs } from '../generated-graphql-types';
|
|
3
|
+
import { ChannelAccessService } from '../services/channel-access.service';
|
|
4
|
+
import { ChannelProductAvailabilityService } from '../services/channel-product-availability.service';
|
|
5
|
+
export declare class ChannelProductAvailabilityAdminResolver {
|
|
6
|
+
private availabilityService;
|
|
7
|
+
private channelAccess;
|
|
8
|
+
private connection;
|
|
9
|
+
constructor(availabilityService: ChannelProductAvailabilityService, channelAccess: ChannelAccessService, connection: TransactionalConnection);
|
|
10
|
+
productChannelAvailability(ctx: RequestContext, args: QueryProductChannelAvailabilityArgs): Promise<import("../services/channel-product-availability.service").ProductChannelAvailabilityResult>;
|
|
11
|
+
productChannelAvailabilities(ctx: RequestContext, args: QueryProductChannelAvailabilitiesArgs): Promise<import("../services/channel-product-availability.service").ProductChannelAvailabilityResult[]>;
|
|
12
|
+
productChannelAvailabilityOverrides(ctx: RequestContext, args: QueryProductChannelAvailabilityOverridesArgs): Promise<{
|
|
13
|
+
items: import("..").ProductChannelAvailability[];
|
|
14
|
+
totalItems: number;
|
|
15
|
+
}>;
|
|
16
|
+
setProductChannelAvailability(ctx: RequestContext, args: MutationSetProductChannelAvailabilityArgs): Promise<import("../services/channel-product-availability.service").ProductChannelAvailabilityResult>;
|
|
17
|
+
clearProductChannelAvailability(ctx: RequestContext, args: MutationClearProductChannelAvailabilityArgs): Promise<import("../services/channel-product-availability.service").ProductChannelAvailabilityResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ChannelProductAvailabilityAdminResolver = void 0;
|
|
16
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
17
|
+
const core_1 = require("@vendure/core");
|
|
18
|
+
const channel_access_service_1 = require("../services/channel-access.service");
|
|
19
|
+
const channel_product_availability_service_1 = require("../services/channel-product-availability.service");
|
|
20
|
+
let ChannelProductAvailabilityAdminResolver = class ChannelProductAvailabilityAdminResolver {
|
|
21
|
+
constructor(availabilityService, channelAccess, connection) {
|
|
22
|
+
this.availabilityService = availabilityService;
|
|
23
|
+
this.channelAccess = channelAccess;
|
|
24
|
+
this.connection = connection;
|
|
25
|
+
}
|
|
26
|
+
async productChannelAvailability(ctx, args) {
|
|
27
|
+
var _a;
|
|
28
|
+
const channelId = (_a = args.channelId) !== null && _a !== void 0 ? _a : ctx.channelId;
|
|
29
|
+
this.channelAccess.assertPermissionInChannel(ctx, channelId, channel_access_service_1.READ_PERMISSIONS);
|
|
30
|
+
return this.availabilityService.getAvailabilityForChannel(ctx, args.productId, channelId);
|
|
31
|
+
}
|
|
32
|
+
async productChannelAvailabilities(ctx, args) {
|
|
33
|
+
const product = await this.connection.getEntityOrThrow(ctx, core_1.Product, args.productId, {
|
|
34
|
+
relations: ['channels'],
|
|
35
|
+
loadEagerRelations: false,
|
|
36
|
+
});
|
|
37
|
+
const readable = new Set(this.channelAccess.channelIdsWithPermission(ctx, channel_access_service_1.READ_PERMISSIONS));
|
|
38
|
+
const channelIds = product.channels.map(c => c.id).filter(id => readable.has(String(id)));
|
|
39
|
+
return this.availabilityService.getAvailabilityForChannels(ctx, product.id, channelIds);
|
|
40
|
+
}
|
|
41
|
+
async productChannelAvailabilityOverrides(ctx, args) {
|
|
42
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
43
|
+
const channelId = (_b = (_a = args.options) === null || _a === void 0 ? void 0 : _a.channelId) !== null && _b !== void 0 ? _b : ctx.channelId;
|
|
44
|
+
this.channelAccess.assertPermissionInChannel(ctx, channelId, channel_access_service_1.READ_PERMISSIONS);
|
|
45
|
+
return this.availabilityService.listOverrides(ctx, channelId, {
|
|
46
|
+
productId: (_d = (_c = args.options) === null || _c === void 0 ? void 0 : _c.productId) !== null && _d !== void 0 ? _d : undefined,
|
|
47
|
+
enabled: (_f = (_e = args.options) === null || _e === void 0 ? void 0 : _e.enabled) !== null && _f !== void 0 ? _f : undefined,
|
|
48
|
+
skip: (_h = (_g = args.options) === null || _g === void 0 ? void 0 : _g.skip) !== null && _h !== void 0 ? _h : undefined,
|
|
49
|
+
take: (_k = (_j = args.options) === null || _j === void 0 ? void 0 : _j.take) !== null && _k !== void 0 ? _k : undefined,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async setProductChannelAvailability(ctx, args) {
|
|
53
|
+
var _a;
|
|
54
|
+
const channelId = (_a = args.input.channelId) !== null && _a !== void 0 ? _a : ctx.channelId;
|
|
55
|
+
this.channelAccess.assertPermissionInChannel(ctx, channelId, channel_access_service_1.UPDATE_PERMISSIONS);
|
|
56
|
+
return this.availabilityService.setOverride(ctx, args.input.productId, args.input.enabled, channelId);
|
|
57
|
+
}
|
|
58
|
+
async clearProductChannelAvailability(ctx, args) {
|
|
59
|
+
var _a;
|
|
60
|
+
const channelId = (_a = args.input.channelId) !== null && _a !== void 0 ? _a : ctx.channelId;
|
|
61
|
+
this.channelAccess.assertPermissionInChannel(ctx, channelId, channel_access_service_1.UPDATE_PERMISSIONS);
|
|
62
|
+
return this.availabilityService.clearOverride(ctx, args.input.productId, channelId);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
exports.ChannelProductAvailabilityAdminResolver = ChannelProductAvailabilityAdminResolver;
|
|
66
|
+
__decorate([
|
|
67
|
+
(0, graphql_1.Query)(),
|
|
68
|
+
(0, core_1.Allow)(...channel_access_service_1.READ_PERMISSIONS),
|
|
69
|
+
__param(0, (0, core_1.Ctx)()),
|
|
70
|
+
__param(1, (0, graphql_1.Args)()),
|
|
71
|
+
__metadata("design:type", Function),
|
|
72
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
73
|
+
__metadata("design:returntype", Promise)
|
|
74
|
+
], ChannelProductAvailabilityAdminResolver.prototype, "productChannelAvailability", null);
|
|
75
|
+
__decorate([
|
|
76
|
+
(0, graphql_1.Query)(),
|
|
77
|
+
(0, core_1.Allow)(...channel_access_service_1.READ_PERMISSIONS),
|
|
78
|
+
__param(0, (0, core_1.Ctx)()),
|
|
79
|
+
__param(1, (0, graphql_1.Args)()),
|
|
80
|
+
__metadata("design:type", Function),
|
|
81
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
82
|
+
__metadata("design:returntype", Promise)
|
|
83
|
+
], ChannelProductAvailabilityAdminResolver.prototype, "productChannelAvailabilities", null);
|
|
84
|
+
__decorate([
|
|
85
|
+
(0, graphql_1.Query)(),
|
|
86
|
+
(0, core_1.Allow)(...channel_access_service_1.READ_PERMISSIONS),
|
|
87
|
+
__param(0, (0, core_1.Ctx)()),
|
|
88
|
+
__param(1, (0, graphql_1.Args)()),
|
|
89
|
+
__metadata("design:type", Function),
|
|
90
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
91
|
+
__metadata("design:returntype", Promise)
|
|
92
|
+
], ChannelProductAvailabilityAdminResolver.prototype, "productChannelAvailabilityOverrides", null);
|
|
93
|
+
__decorate([
|
|
94
|
+
(0, core_1.Transaction)(),
|
|
95
|
+
(0, graphql_1.Mutation)(),
|
|
96
|
+
(0, core_1.Allow)(...channel_access_service_1.UPDATE_PERMISSIONS),
|
|
97
|
+
__param(0, (0, core_1.Ctx)()),
|
|
98
|
+
__param(1, (0, graphql_1.Args)()),
|
|
99
|
+
__metadata("design:type", Function),
|
|
100
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
101
|
+
__metadata("design:returntype", Promise)
|
|
102
|
+
], ChannelProductAvailabilityAdminResolver.prototype, "setProductChannelAvailability", null);
|
|
103
|
+
__decorate([
|
|
104
|
+
(0, core_1.Transaction)(),
|
|
105
|
+
(0, graphql_1.Mutation)(),
|
|
106
|
+
(0, core_1.Allow)(...channel_access_service_1.UPDATE_PERMISSIONS),
|
|
107
|
+
__param(0, (0, core_1.Ctx)()),
|
|
108
|
+
__param(1, (0, graphql_1.Args)()),
|
|
109
|
+
__metadata("design:type", Function),
|
|
110
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
111
|
+
__metadata("design:returntype", Promise)
|
|
112
|
+
], ChannelProductAvailabilityAdminResolver.prototype, "clearProductChannelAvailability", null);
|
|
113
|
+
exports.ChannelProductAvailabilityAdminResolver = ChannelProductAvailabilityAdminResolver = __decorate([
|
|
114
|
+
(0, graphql_1.Resolver)(),
|
|
115
|
+
__metadata("design:paramtypes", [channel_product_availability_service_1.ChannelProductAvailabilityService,
|
|
116
|
+
channel_access_service_1.ChannelAccessService,
|
|
117
|
+
core_1.TransactionalConnection])
|
|
118
|
+
], ChannelProductAvailabilityAdminResolver);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const adminApiExtensions: import("graphql").DocumentNode;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.adminApiExtensions = void 0;
|
|
7
|
+
const graphql_tag_1 = __importDefault(require("graphql-tag"));
|
|
8
|
+
exports.adminApiExtensions = (0, graphql_tag_1.default) `
|
|
9
|
+
"""
|
|
10
|
+
INHERITED: no override, the global Product.enabled applies.
|
|
11
|
+
ENABLED / DISABLED: explicit channel override, takes precedence over Product.enabled.
|
|
12
|
+
"""
|
|
13
|
+
enum ProductChannelAvailabilityOverrideState {
|
|
14
|
+
INHERITED
|
|
15
|
+
ENABLED
|
|
16
|
+
DISABLED
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ProductChannelAvailability {
|
|
20
|
+
productId: ID!
|
|
21
|
+
channelId: ID!
|
|
22
|
+
channelCode: String!
|
|
23
|
+
"The global Product.enabled value."
|
|
24
|
+
globalEnabled: Boolean!
|
|
25
|
+
"The explicit override for this channel, or null when inherited."
|
|
26
|
+
override: Boolean
|
|
27
|
+
overrideState: ProductChannelAvailabilityOverrideState!
|
|
28
|
+
"What the storefront and checkout enforce in this channel."
|
|
29
|
+
effectiveEnabled: Boolean!
|
|
30
|
+
productInChannel: Boolean!
|
|
31
|
+
updatedAt: DateTime
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ProductChannelAvailabilityOverride {
|
|
35
|
+
id: ID!
|
|
36
|
+
createdAt: DateTime!
|
|
37
|
+
updatedAt: DateTime!
|
|
38
|
+
productId: ID!
|
|
39
|
+
channelId: ID!
|
|
40
|
+
enabled: Boolean!
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ProductChannelAvailabilityOverrideList {
|
|
44
|
+
items: [ProductChannelAvailabilityOverride!]!
|
|
45
|
+
totalItems: Int!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
input ProductChannelAvailabilityOverrideListOptions {
|
|
49
|
+
"Defaults to the active channel."
|
|
50
|
+
channelId: ID
|
|
51
|
+
productId: ID
|
|
52
|
+
enabled: Boolean
|
|
53
|
+
skip: Int
|
|
54
|
+
take: Int
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
input SetProductChannelAvailabilityInput {
|
|
58
|
+
productId: ID!
|
|
59
|
+
"Defaults to the active channel."
|
|
60
|
+
channelId: ID
|
|
61
|
+
enabled: Boolean!
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input ClearProductChannelAvailabilityInput {
|
|
65
|
+
productId: ID!
|
|
66
|
+
"Defaults to the active channel."
|
|
67
|
+
channelId: ID
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
extend type Query {
|
|
71
|
+
"Availability of a product in one channel (default: the active channel)."
|
|
72
|
+
productChannelAvailability(productId: ID!, channelId: ID): ProductChannelAvailability!
|
|
73
|
+
"Availability of a product in every channel it is assigned to that the current administrator can read."
|
|
74
|
+
productChannelAvailabilities(productId: ID!): [ProductChannelAvailability!]!
|
|
75
|
+
"Explicit overrides in a channel (default: the active channel)."
|
|
76
|
+
productChannelAvailabilityOverrides(
|
|
77
|
+
options: ProductChannelAvailabilityOverrideListOptions
|
|
78
|
+
): ProductChannelAvailabilityOverrideList!
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
extend type Mutation {
|
|
82
|
+
"Sets an explicit override. Does not change Product.enabled or channel assignment."
|
|
83
|
+
setProductChannelAvailability(input: SetProductChannelAvailabilityInput!): ProductChannelAvailability!
|
|
84
|
+
"Removes the override so the product inherits Product.enabled again."
|
|
85
|
+
clearProductChannelAvailability(input: ClearProductChannelAvailabilityInput!): ProductChannelAvailability!
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ActiveOrderService, Order, RelationPaths, RequestContext } from '@vendure/core';
|
|
2
|
+
import { OverridePurchaseService } from '../order/override-purchase.service';
|
|
3
|
+
/**
|
|
4
|
+
* Overrides the Shop API add-to-cart mutations. Same decorators and semantics as
|
|
5
|
+
* core; items whose product is globally disabled but explicitly enabled in the
|
|
6
|
+
* channel are routed around core's hard `product.enabled` check.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ShopOrderAvailabilityResolver {
|
|
9
|
+
private activeOrderService;
|
|
10
|
+
private purchaseService;
|
|
11
|
+
constructor(activeOrderService: ActiveOrderService, purchaseService: OverridePurchaseService);
|
|
12
|
+
addItemToOrder(ctx: RequestContext, args: any, relations: RelationPaths<Order>): Promise<import("../order/override-purchase.service").OrderErrorResult | Order>;
|
|
13
|
+
addItemsToOrder(ctx: RequestContext, args: any): Promise<{
|
|
14
|
+
order: Order;
|
|
15
|
+
errorResults: import("../order/override-purchase.service").OrderErrorResult[];
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Overrides the Admin API `addItemToDraftOrder` mutation with the same routing.
|
|
20
|
+
*/
|
|
21
|
+
export declare class AdminDraftOrderAvailabilityResolver {
|
|
22
|
+
private purchaseService;
|
|
23
|
+
constructor(purchaseService: OverridePurchaseService);
|
|
24
|
+
addItemToDraftOrder(ctx: RequestContext, args: any): Promise<import("../order/override-purchase.service").OrderErrorResult | Order>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AdminDraftOrderAvailabilityResolver = exports.ShopOrderAvailabilityResolver = void 0;
|
|
16
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
17
|
+
const core_1 = require("@vendure/core");
|
|
18
|
+
const override_purchase_service_1 = require("../order/override-purchase.service");
|
|
19
|
+
/**
|
|
20
|
+
* Overrides the Shop API add-to-cart mutations. Same decorators and semantics as
|
|
21
|
+
* core; items whose product is globally disabled but explicitly enabled in the
|
|
22
|
+
* channel are routed around core's hard `product.enabled` check.
|
|
23
|
+
*/
|
|
24
|
+
let ShopOrderAvailabilityResolver = class ShopOrderAvailabilityResolver {
|
|
25
|
+
constructor(activeOrderService, purchaseService) {
|
|
26
|
+
this.activeOrderService = activeOrderService;
|
|
27
|
+
this.purchaseService = purchaseService;
|
|
28
|
+
}
|
|
29
|
+
async addItemToOrder(ctx, args, relations) {
|
|
30
|
+
const order = await this.activeOrderService.getActiveOrder(ctx, args[core_1.ACTIVE_ORDER_INPUT_FIELD_NAME], true);
|
|
31
|
+
return this.purchaseService.addItemToOrder(ctx, order.id, { productVariantId: args.productVariantId, quantity: args.quantity, customFields: args.customFields }, relations);
|
|
32
|
+
}
|
|
33
|
+
async addItemsToOrder(ctx, args) {
|
|
34
|
+
const order = await this.activeOrderService.getActiveOrder(ctx, args[core_1.ACTIVE_ORDER_INPUT_FIELD_NAME], true);
|
|
35
|
+
const result = await this.purchaseService.addItemsToOrder(ctx, order.id, args.inputs);
|
|
36
|
+
return { order: result.order, errorResults: result.errorResults };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
exports.ShopOrderAvailabilityResolver = ShopOrderAvailabilityResolver;
|
|
40
|
+
__decorate([
|
|
41
|
+
(0, core_1.Transaction)(),
|
|
42
|
+
(0, graphql_1.Mutation)(),
|
|
43
|
+
(0, core_1.Allow)(core_1.Permission.UpdateOrder, core_1.Permission.Owner),
|
|
44
|
+
__param(0, (0, core_1.Ctx)()),
|
|
45
|
+
__param(1, (0, graphql_1.Args)()),
|
|
46
|
+
__param(2, (0, core_1.Relations)({ entity: core_1.Order, omit: ['aggregateOrder', 'sellerOrders'] })),
|
|
47
|
+
__metadata("design:type", Function),
|
|
48
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object, Array]),
|
|
49
|
+
__metadata("design:returntype", Promise)
|
|
50
|
+
], ShopOrderAvailabilityResolver.prototype, "addItemToOrder", null);
|
|
51
|
+
__decorate([
|
|
52
|
+
(0, core_1.Transaction)(),
|
|
53
|
+
(0, graphql_1.Mutation)(),
|
|
54
|
+
(0, core_1.Allow)(core_1.Permission.UpdateOrder, core_1.Permission.Owner),
|
|
55
|
+
__param(0, (0, core_1.Ctx)()),
|
|
56
|
+
__param(1, (0, graphql_1.Args)()),
|
|
57
|
+
__metadata("design:type", Function),
|
|
58
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
59
|
+
__metadata("design:returntype", Promise)
|
|
60
|
+
], ShopOrderAvailabilityResolver.prototype, "addItemsToOrder", null);
|
|
61
|
+
exports.ShopOrderAvailabilityResolver = ShopOrderAvailabilityResolver = __decorate([
|
|
62
|
+
(0, graphql_1.Resolver)(),
|
|
63
|
+
__metadata("design:paramtypes", [core_1.ActiveOrderService,
|
|
64
|
+
override_purchase_service_1.OverridePurchaseService])
|
|
65
|
+
], ShopOrderAvailabilityResolver);
|
|
66
|
+
/**
|
|
67
|
+
* Overrides the Admin API `addItemToDraftOrder` mutation with the same routing.
|
|
68
|
+
*/
|
|
69
|
+
let AdminDraftOrderAvailabilityResolver = class AdminDraftOrderAvailabilityResolver {
|
|
70
|
+
constructor(purchaseService) {
|
|
71
|
+
this.purchaseService = purchaseService;
|
|
72
|
+
}
|
|
73
|
+
async addItemToDraftOrder(ctx, args) {
|
|
74
|
+
const { orderId, input } = args;
|
|
75
|
+
return this.purchaseService.addItemToOrder(ctx, orderId, {
|
|
76
|
+
productVariantId: input.productVariantId,
|
|
77
|
+
quantity: input.quantity,
|
|
78
|
+
customFields: input.customFields,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
exports.AdminDraftOrderAvailabilityResolver = AdminDraftOrderAvailabilityResolver;
|
|
83
|
+
__decorate([
|
|
84
|
+
(0, core_1.Transaction)(),
|
|
85
|
+
(0, graphql_1.Mutation)(),
|
|
86
|
+
(0, core_1.Allow)(core_1.Permission.CreateOrder),
|
|
87
|
+
__param(0, (0, core_1.Ctx)()),
|
|
88
|
+
__param(1, (0, graphql_1.Args)()),
|
|
89
|
+
__metadata("design:type", Function),
|
|
90
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
91
|
+
__metadata("design:returntype", Promise)
|
|
92
|
+
], AdminDraftOrderAvailabilityResolver.prototype, "addItemToDraftOrder", null);
|
|
93
|
+
exports.AdminDraftOrderAvailabilityResolver = AdminDraftOrderAvailabilityResolver = __decorate([
|
|
94
|
+
(0, graphql_1.Resolver)(),
|
|
95
|
+
__metadata("design:paramtypes", [override_purchase_service_1.OverridePurchaseService])
|
|
96
|
+
], AdminDraftOrderAvailabilityResolver);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ApiType, Collection, Product, ProductService, ProductVariant, ProductVariantService, RelationPaths, RequestContext, TransactionalConnection } from '@vendure/core';
|
|
2
|
+
import { ChannelProductAvailabilityService } from '../services/channel-product-availability.service';
|
|
3
|
+
/**
|
|
4
|
+
* Overrides the Shop API `products` and `product` queries. Identical to core except
|
|
5
|
+
* that the `enabled` guard is replaced by the channel-aware rule, applied in SQL so
|
|
6
|
+
* that counts and pagination stay correct.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ShopCatalogAvailabilityResolver {
|
|
9
|
+
private productService;
|
|
10
|
+
private availabilityService;
|
|
11
|
+
constructor(productService: ProductService, availabilityService: ChannelProductAvailabilityService);
|
|
12
|
+
products(ctx: RequestContext, args: any, relations: RelationPaths<Product>): Promise<import("@vendure/core").PaginatedList<import("@vendure/core").Translated<Product>>>;
|
|
13
|
+
product(ctx: RequestContext, args: {
|
|
14
|
+
id?: string;
|
|
15
|
+
slug?: string;
|
|
16
|
+
}, relations: RelationPaths<Product>): Promise<Product | undefined>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Overrides `Collection.productVariants` in the Shop API so that variants of
|
|
20
|
+
* products explicitly enabled in the channel are listed, and variants of products
|
|
21
|
+
* explicitly disabled are not. The Admin API delegates to core unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ShopCollectionAvailabilityResolver {
|
|
24
|
+
private productVariantService;
|
|
25
|
+
private connection;
|
|
26
|
+
constructor(productVariantService: ProductVariantService, connection: TransactionalConnection);
|
|
27
|
+
productVariants(ctx: RequestContext, collection: Collection, args: any, apiType: ApiType, relations: RelationPaths<ProductVariant>): Promise<import("@vendure/core").PaginatedList<import("@vendure/core").Translated<ProductVariant>>>;
|
|
28
|
+
/**
|
|
29
|
+
* Products with variants in the collection that are effectively disabled in the
|
|
30
|
+
* active channel: globally disabled without an enabling override, or explicitly
|
|
31
|
+
* disabled.
|
|
32
|
+
*/
|
|
33
|
+
private getEffectivelyDisabledProductIds;
|
|
34
|
+
}
|