<commit_before>const {UpdateCollisionError, NotFoundError, MethodNotAllowedError} = require('@tryghost/errors');

/**
 * @typedef {object} ProductModel
 */

/**
 * @typedef {object} StripePriceInput
 * @param {string} nickname
 * @param {string} currency
 * @param {number} amount
 * @param {'recurring'|'one-time'} type
 * @param {string | null} interval
 * @param {string?} stripe_product_id
 * @param {string?} stripe_price_id
 */

/**
 * @typedef {object} BenefitInput
 * @param {string} name
 */

class ProductRepository {
    /**
     * @param {object} deps
     * @param {any} deps.Product
     * @param {any} deps.StripeProduct
     * @param {any} deps.StripePrice
     * @param {import('@tryghost/members-api/lib/services/stripe-api')} deps.stripeAPIService
     */
    constructor({
        Product,
        StripeProduct,
        StripePrice,
        stripeAPIService
    }) {
        this._Product = Product;
        this._StripeProduct = StripeProduct;
        this._StripePrice = StripePrice;
        this._stripeAPIService = stripeAPIService;
    }

    /**
     * Retrieves a Product by either stripe_product_id, stripe_price_id, id or slug
     *
     * @param {{stripe_product_id: string} | {stripe_price_id: string} | {id: string} | {slug: string}} data
     * @param {object} options
     *
     * @returns {Promise<ProductModel>}
     */
    async get(data, options = {}) {
        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.get(data, {
                    ...options,
                    transacting
                });
            });
        }
        if ('stripe_product_id' in data) {
            const stripeProduct = await this._StripeProduct.findOne({
                stripe_product_id: data.stripe_product_id
            }, options);

            if (!stripeProduct) {
                return null;
            }

            return await stripeProduct.related('product').fetch(options);
        }

        if ('stripe_price_id' in data) {
            const stripePrice = await this._StripePrice.findOne({
                stripe_price_id: data.stripe_price_id
            }, options);

            if (!stripePrice) {
                return null;
            }

            const stripeProduct = await stripePrice.related('stripeProduct').fetch(options);

            if (!stripeProduct) {
                return null;
            }

            return await stripeProduct.related('product').fetch(options);
        }

        if ('id' in data) {
            return await this._Product.findOne({id: data.id}, options);
        }

        if ('slug' in data) {
            return await this._Product.findOne({slug: data.slug}, options);
        }

        throw new NotFoundError('Missing id, slug, stripe_product_id or stripe_price_id from data');
    }

    /**
     * Creates a product from a name
     *
     * @param {object} data
     * @param {string} data.name
     * @param {string} data.description
     * @param {BenefitInput[]} data.benefits
     * @param {StripePriceInput[]} data.stripe_prices
     * @param {StripePriceInput|null} data.monthly_price
     * @param {StripePriceInput|null} data.yearly_price
     * @param {string} data.product_id
     * @param {string} data.stripe_product_id
     *
     * @param {object} options
     *
     * @returns {Promise<ProductModel>}
     **/
    async create(data, options = {}) {
        if (!this._stripeAPIService.configured && (data.stripe_prices || data.monthly_price || data.yearly_price)) {
            throw new UpdateCollisionError({
                message: 'The requested functionality requires Stripe to be configured. See https://ghost.org/integrations/stripe/',
                code: 'STRIPE_NOT_CONFIGURED'
            });
        }

        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.create(data, {
                    ...options,
                    transacting
                });
            });
        }

        const productData = {
            type: 'paid',
            name: data.name,
            description: data.description,
            benefits: data.benefits
        };

        const product = await this._Product.add(productData, options);

        if (this._stripeAPIService.configured) {
            const stripeProduct = await this._stripeAPIService.createProduct({
                name: productData.name
            });

            await this._StripeProduct.add({
                product_id: product.id,
                stripe_product_id: stripeProduct.id
            }, options);

            if (data.monthly_price || data.yearly_price) {
                if (data.monthly_price) {
                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.id,
                        active: true,
                        nickname: `Monthly`,
                        currency: data.monthly_price.currency,
                        amount: data.monthly_price.amount,
                        type: 'recurring',
                        interval: 'month'
                    });

                    const stripePrice = await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.id,
                        active: true,
                        nickname: price.nickname,
                        currency: price.currency,
                        amount: price.unit_amount,
                        type: 'recurring',
                        interval: 'month'
                    }, options);

                    await this._Product.edit({monthly_price_id: stripePrice.id}, {id: product.id, transacting: options.transacting});
                }

                if (data.yearly_price) {
                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.id,
                        active: true,
                        nickname: `Yearly`,
                        currency: data.yearly_price.currency,
                        amount: data.yearly_price.amount,
                        type: 'recurring',
                        interval: 'year'
                    });

                    const stripePrice = await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.id,
                        active: true,
                        nickname: price.nickname,
                        currency: price.currency,
                        amount: price.unit_amount,
                        type: 'recurring',
                        interval: 'year'
                    }, options);

                    await this._Product.edit({yearly_price_id: stripePrice.id}, {id: product.id, transacting: options.transacting});
                }
            } else if (data.stripe_prices) {
                for (const newPrice of data.stripe_prices) {
                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.id,
                        active: true,
                        nickname: newPrice.nickname,
                        currency: newPrice.currency,
                        amount: newPrice.amount,
                        type: newPrice.type,
                        interval: newPrice.interval
                    });

                    await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.id,
                        active: true,
                        nickname: newPrice.nickname,
                        currency: newPrice.currency,
                        amount: newPrice.amount,
                        type: newPrice.type,
                        interval: newPrice.interval
                    }, options);
                }
            }

            await product.related('stripePrices').fetch(options);
            await product.related('monthlyPrice').fetch(options);
            await product.related('yearlyPrice').fetch(options);
        }

        return product;
    }

    /**
     * Updates a product by id
     *
     * @param {object} data
     * @param {string} data.id
     * @param {string} data.name
     * @param {string} data.description
     * @param {BenefitInput[]} data.benefits
     *
     * @param {StripePriceInput[]=} data.stripe_prices
     * @param {StripePriceInput|null} data.monthly_price
     * @param {StripePriceInput|null} data.yearly_price
     *
     * @param {object} options
     *
     * @returns {Promise<ProductModel>}
     **/
    async update(data, options = {}) {
        if (!this._stripeAPIService.configured && (data.stripe_prices || data.monthly_price || data.yearly_price)) {
            throw new UpdateCollisionError({
                message: 'The requested functionality requires Stripe to be configured. See https://ghost.org/integrations/stripe/',
                code: 'STRIPE_NOT_CONFIGURED'
            });
        }

        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.update(data, {
                    ...options,
                    transacting
                });
            });
        }
        const productId = data.id || options.id;

        const existingProduct = await this._Product.findOne({id: productId}, options);

        let productData = {
            name: data.name,
            description: data.description,
            benefits: data.benefits
        };

        if (existingProduct.get('type') === 'free') {
            delete productData.name;
        }

        let product = await this._Product.edit(productData, {
            ...options,
            id: productId
        });

        if (this._stripeAPIService.configured && product.get('type') !== 'free') {
            await product.related('stripeProducts').fetch(options);

            if (!product.related('stripeProducts').first()) {
                const stripeProduct = await this._stripeAPIService.createProduct({
                    name: product.get('name')
                });

                await this._StripeProduct.add({
                    product_id: product.id,
                    stripe_product_id: stripeProduct.id
                }, options);

                await product.related('stripeProducts').fetch(options);
            } else {
                if (product.attributes.name !== product._previousAttributes.name) {
                    const stripeProduct = product.related('stripeProducts').first();
                    await this._stripeAPIService.updateProduct(stripeProduct.get('stripe_product_id'), {
                        name: product.get('name')
                    });
                }
            }

            const defaultStripeProduct = product.related('stripeProducts').first();

            if (data.monthly_price || data.yearly_price) {
                if (data.monthly_price) {
                    const existingPrice = await this._StripePrice.findOne({
                        stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                        amount: data.monthly_price.amount,
                        currency: data.monthly_price.currency,
                        type: 'recurring',
                        interval: 'month'
                    }, options);
                    let priceModel;
                    if (existingPrice) {
                        priceModel = existingPrice;

                        await this._stripeAPIService.updatePrice(priceModel.get('stripe_price_id'), {
                            active: true
                        });

                        await this._StripePrice.edit({
                            active: true
                        }, {...options, id: priceModel.id});
                    } else {
                        const price = await this._stripeAPIService.createPrice({
                            product: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: `Monthly`,
                            currency: data.monthly_price.currency,
                            amount: data.monthly_price.amount,
                            type: 'recurring',
                            interval: 'month'
                        });

                        const stripePrice = await this._StripePrice.add({
                            stripe_price_id: price.id,
                            stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: price.nickname,
                            currency: price.currency,
                            amount: price.unit_amount,
                            type: 'recurring',
                            interval: 'month'
                        }, options);

                        priceModel = stripePrice;
                    }

                    product = await this._Product.edit({monthly_price_id: priceModel.id}, {...options, id: product.id});
                }

                if (data.yearly_price) {
                    const existingPrice = await this._StripePrice.findOne({
                        stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                        amount: data.yearly_price.amount,
                        currency: data.yearly_price.currency,
                        type: 'recurring',
                        interval: 'year'
                    }, options);
                    let priceModel;

                    if (existingPrice) {
                        priceModel = existingPrice;

                        await this._stripeAPIService.updatePrice(priceModel.get('stripe_price_id'), {
                            active: true
                        });

                        await this._StripePrice.edit({
                            active: true
                        }, {...options, id: priceModel.id});
                    } else {
                        const price = await this._stripeAPIService.createPrice({
                            product: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: `Yearly`,
                            currency: data.yearly_price.currency,
                            amount: data.yearly_price.amount,
                            type: 'recurring',
                            interval: 'year'
                        });

                        const stripePrice = await this._StripePrice.add({
                            stripe_price_id: price.id,
                            stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: price.nickname,
                            currency: price.currency,
                            amount: price.unit_amount,
                            type: 'recurring',
                            interval: 'year'
                        }, options);

                        priceModel = stripePrice;
                    }

                    product = await this._Product.edit({yearly_price_id: priceModel.id}, {...options, id: product.id});
                }
            } else if (data.stripe_prices) {
                const newPrices = data.stripe_prices.filter(price => !price.stripe_price_id);
                const existingPrices = data.stripe_prices.filter((price) => {
                    return !!price.stripe_price_id && !!price.stripe_product_id;
                });

                for (const existingPrice of existingPrices) {
                    const productId = existingPrice.stripe_product_id;
                    let stripeProduct = await this._StripeProduct.findOne({stripe_product_id: productId}, options);
                    if (!stripeProduct) {
                        stripeProduct = await this._StripeProduct.add({
                            product_id: product.id,
                            stripe_product_id: productId
                        }, options);
                    }
                    const stripePrice = await this._StripePrice.findOne({stripe_price_id: existingPrice.stripe_price_id}, options);

                    if (!stripePrice) {
                        await this._StripePrice.add({
                            stripe_price_id: existingPrice.stripe_price_id,
                            stripe_product_id: stripeProduct.get('stripe_product_id'),
                            active: existingPrice.active,
                            nickname: existingPrice.nickname,
                            description: existingPrice.description,
                            currency: existingPrice.currency,
                            amount: existingPrice.amount,
                            type: existingPrice.type,
                            interval: existingPrice.interval
                        }, options);
                    } else {
                        const updated = await this._StripePrice.edit({
                            nickname: existingPrice.nickname,
                            description: existingPrice.description,
                            active: existingPrice.active
                        }, {
                            ...options,
                            id: stripePrice.id
                        });

                        await this._stripeAPIService.updatePrice(updated.get('stripe_price_id'), {
                            nickname: updated.get('nickname'),
                            active: updated.get('active')
                        });
                    }
                }

                for (const newPrice of newPrices) {
                    const productId = newPrice.stripe_product_id;
                    const stripeProduct = productId ?
                        await this._StripeProduct.findOne({stripe_product_id: productId}, options) : defaultStripeProduct;

                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.get('stripe_product_id'),
                        active: true,
                        nickname: newPrice.nickname,
                        currency: newPrice.currency,
                        amount: newPrice.amount,
                        type: newPrice.type,
                        interval: newPrice.interval
                    });

                    await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.get('stripe_product_id'),
                        active: price.active,
                        nickname: price.nickname,
                        description: newPrice.description,
                        currency: price.currency,
                        amount: price.unit_amount,
                        type: price.type,
                        interval: price.recurring && price.recurring.interval || null
                    }, options);
                }
            }

            await product.related('stripePrices').fetch(options);
            await product.related('monthlyPrice').fetch(options);
            await product.related('yearlyPrice').fetch(options);
            await product.related('benefits').fetch(options);
        }

        return product;
    }

    /**
     * Returns a paginated list of Products
     *
     * @params {object} options
     *
     * @returns {Promise<{data: ProductModel[], meta: object}>}
     **/
    async list(options = {}) {
        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.list({
                    ...options,
                    transacting
                });
            });
        }
        return this._Product.findPage(options);
    }

    async destroy() {
        throw new MethodNotAllowedError('Cannot destroy products, yet...');
    }
}

module.exports = ProductRepository;
<commit_msg>Added support for the active flag to Products API<commit_after>const {UpdateCollisionError, NotFoundError, MethodNotAllowedError} = require('@tryghost/errors');

/**
 * @typedef {object} ProductModel
 */

/**
 * @typedef {object} StripePriceInput
 * @param {string} nickname
 * @param {string} currency
 * @param {number} amount
 * @param {'recurring'|'one-time'} type
 * @param {string | null} interval
 * @param {string?} stripe_product_id
 * @param {string?} stripe_price_id
 */

/**
 * @typedef {object} BenefitInput
 * @param {string} name
 */

class ProductRepository {
    /**
     * @param {object} deps
     * @param {any} deps.Product
     * @param {any} deps.StripeProduct
     * @param {any} deps.StripePrice
     * @param {import('@tryghost/members-api/lib/services/stripe-api')} deps.stripeAPIService
     */
    constructor({
        Product,
        StripeProduct,
        StripePrice,
        stripeAPIService
    }) {
        this._Product = Product;
        this._StripeProduct = StripeProduct;
        this._StripePrice = StripePrice;
        this._stripeAPIService = stripeAPIService;
    }

    /**
     * Retrieves a Product by either stripe_product_id, stripe_price_id, id or slug
     *
     * @param {{stripe_product_id: string} | {stripe_price_id: string} | {id: string} | {slug: string}} data
     * @param {object} options
     *
     * @returns {Promise<ProductModel>}
     */
    async get(data, options = {}) {
        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.get(data, {
                    ...options,
                    transacting
                });
            });
        }
        if ('stripe_product_id' in data) {
            const stripeProduct = await this._StripeProduct.findOne({
                stripe_product_id: data.stripe_product_id
            }, options);

            if (!stripeProduct) {
                return null;
            }

            return await stripeProduct.related('product').fetch(options);
        }

        if ('stripe_price_id' in data) {
            const stripePrice = await this._StripePrice.findOne({
                stripe_price_id: data.stripe_price_id
            }, options);

            if (!stripePrice) {
                return null;
            }

            const stripeProduct = await stripePrice.related('stripeProduct').fetch(options);

            if (!stripeProduct) {
                return null;
            }

            return await stripeProduct.related('product').fetch(options);
        }

        if ('id' in data) {
            return await this._Product.findOne({id: data.id}, options);
        }

        if ('slug' in data) {
            return await this._Product.findOne({slug: data.slug}, options);
        }

        throw new NotFoundError('Missing id, slug, stripe_product_id or stripe_price_id from data');
    }

    /**
     * Creates a product from a name
     *
     * @param {object} data
     * @param {string} data.name
     * @param {string} data.description
     * @param {BenefitInput[]} data.benefits
     * @param {StripePriceInput[]} data.stripe_prices
     * @param {StripePriceInput|null} data.monthly_price
     * @param {StripePriceInput|null} data.yearly_price
     * @param {string} data.product_id
     * @param {string} data.stripe_product_id
     *
     * @param {object} options
     *
     * @returns {Promise<ProductModel>}
     **/
    async create(data, options = {}) {
        if (!this._stripeAPIService.configured && (data.stripe_prices || data.monthly_price || data.yearly_price)) {
            throw new UpdateCollisionError({
                message: 'The requested functionality requires Stripe to be configured. See https://ghost.org/integrations/stripe/',
                code: 'STRIPE_NOT_CONFIGURED'
            });
        }

        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.create(data, {
                    ...options,
                    transacting
                });
            });
        }

        const productData = {
            type: 'paid',
            active: true,
            name: data.name,
            description: data.description,
            benefits: data.benefits
        };

        const product = await this._Product.add(productData, options);

        if (this._stripeAPIService.configured) {
            const stripeProduct = await this._stripeAPIService.createProduct({
                name: productData.name
            });

            await this._StripeProduct.add({
                product_id: product.id,
                stripe_product_id: stripeProduct.id
            }, options);

            if (data.monthly_price || data.yearly_price) {
                if (data.monthly_price) {
                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.id,
                        active: true,
                        nickname: `Monthly`,
                        currency: data.monthly_price.currency,
                        amount: data.monthly_price.amount,
                        type: 'recurring',
                        interval: 'month'
                    });

                    const stripePrice = await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.id,
                        active: true,
                        nickname: price.nickname,
                        currency: price.currency,
                        amount: price.unit_amount,
                        type: 'recurring',
                        interval: 'month'
                    }, options);

                    await this._Product.edit({monthly_price_id: stripePrice.id}, {id: product.id, transacting: options.transacting});
                }

                if (data.yearly_price) {
                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.id,
                        active: true,
                        nickname: `Yearly`,
                        currency: data.yearly_price.currency,
                        amount: data.yearly_price.amount,
                        type: 'recurring',
                        interval: 'year'
                    });

                    const stripePrice = await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.id,
                        active: true,
                        nickname: price.nickname,
                        currency: price.currency,
                        amount: price.unit_amount,
                        type: 'recurring',
                        interval: 'year'
                    }, options);

                    await this._Product.edit({yearly_price_id: stripePrice.id}, {id: product.id, transacting: options.transacting});
                }
            } else if (data.stripe_prices) {
                for (const newPrice of data.stripe_prices) {
                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.id,
                        active: true,
                        nickname: newPrice.nickname,
                        currency: newPrice.currency,
                        amount: newPrice.amount,
                        type: newPrice.type,
                        interval: newPrice.interval
                    });

                    await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.id,
                        active: true,
                        nickname: newPrice.nickname,
                        currency: newPrice.currency,
                        amount: newPrice.amount,
                        type: newPrice.type,
                        interval: newPrice.interval
                    }, options);
                }
            }

            await product.related('stripePrices').fetch(options);
            await product.related('monthlyPrice').fetch(options);
            await product.related('yearlyPrice').fetch(options);
        }

        return product;
    }

    /**
     * Updates a product by id
     *
     * @param {object} data
     * @param {string} data.id
     * @param {string} data.name
     * @param {string} data.description
     * @param {BenefitInput[]} data.benefits
     *
     * @param {StripePriceInput[]=} data.stripe_prices
     * @param {StripePriceInput|null} data.monthly_price
     * @param {StripePriceInput|null} data.yearly_price
     *
     * @param {object} options
     *
     * @returns {Promise<ProductModel>}
     **/
    async update(data, options = {}) {
        if (!this._stripeAPIService.configured && (data.stripe_prices || data.monthly_price || data.yearly_price)) {
            throw new UpdateCollisionError({
                message: 'The requested functionality requires Stripe to be configured. See https://ghost.org/integrations/stripe/',
                code: 'STRIPE_NOT_CONFIGURED'
            });
        }

        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.update(data, {
                    ...options,
                    transacting
                });
            });
        }
        const productId = data.id || options.id;

        const existingProduct = await this._Product.findOne({id: productId}, options);

        let productData = {
            name: data.name,
            description: data.description,
            benefits: data.benefits
        };

        if (Reflect.has(data, 'active')) {
            productData.active = data.active;
        }

        if (existingProduct.get('type') === 'free') {
            delete productData.name;
            delete productData.active;
        }

        let product = await this._Product.edit(productData, {
            ...options,
            id: productId
        });

        if (this._stripeAPIService.configured && product.get('type') !== 'free') {
            await product.related('stripeProducts').fetch(options);

            if (!product.related('stripeProducts').first()) {
                const stripeProduct = await this._stripeAPIService.createProduct({
                    name: product.get('name')
                });

                await this._StripeProduct.add({
                    product_id: product.id,
                    stripe_product_id: stripeProduct.id
                }, options);

                await product.related('stripeProducts').fetch(options);
            } else {
                if (product.attributes.name !== product._previousAttributes.name) {
                    const stripeProduct = product.related('stripeProducts').first();
                    await this._stripeAPIService.updateProduct(stripeProduct.get('stripe_product_id'), {
                        name: product.get('name')
                    });
                }
            }

            const defaultStripeProduct = product.related('stripeProducts').first();

            if (data.monthly_price || data.yearly_price) {
                if (data.monthly_price) {
                    const existingPrice = await this._StripePrice.findOne({
                        stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                        amount: data.monthly_price.amount,
                        currency: data.monthly_price.currency,
                        type: 'recurring',
                        interval: 'month'
                    }, options);
                    let priceModel;
                    if (existingPrice) {
                        priceModel = existingPrice;

                        await this._stripeAPIService.updatePrice(priceModel.get('stripe_price_id'), {
                            active: true
                        });

                        await this._StripePrice.edit({
                            active: true
                        }, {...options, id: priceModel.id});
                    } else {
                        const price = await this._stripeAPIService.createPrice({
                            product: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: `Monthly`,
                            currency: data.monthly_price.currency,
                            amount: data.monthly_price.amount,
                            type: 'recurring',
                            interval: 'month'
                        });

                        const stripePrice = await this._StripePrice.add({
                            stripe_price_id: price.id,
                            stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: price.nickname,
                            currency: price.currency,
                            amount: price.unit_amount,
                            type: 'recurring',
                            interval: 'month'
                        }, options);

                        priceModel = stripePrice;
                    }

                    product = await this._Product.edit({monthly_price_id: priceModel.id}, {...options, id: product.id});
                }

                if (data.yearly_price) {
                    const existingPrice = await this._StripePrice.findOne({
                        stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                        amount: data.yearly_price.amount,
                        currency: data.yearly_price.currency,
                        type: 'recurring',
                        interval: 'year'
                    }, options);
                    let priceModel;

                    if (existingPrice) {
                        priceModel = existingPrice;

                        await this._stripeAPIService.updatePrice(priceModel.get('stripe_price_id'), {
                            active: true
                        });

                        await this._StripePrice.edit({
                            active: true
                        }, {...options, id: priceModel.id});
                    } else {
                        const price = await this._stripeAPIService.createPrice({
                            product: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: `Yearly`,
                            currency: data.yearly_price.currency,
                            amount: data.yearly_price.amount,
                            type: 'recurring',
                            interval: 'year'
                        });

                        const stripePrice = await this._StripePrice.add({
                            stripe_price_id: price.id,
                            stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
                            active: true,
                            nickname: price.nickname,
                            currency: price.currency,
                            amount: price.unit_amount,
                            type: 'recurring',
                            interval: 'year'
                        }, options);

                        priceModel = stripePrice;
                    }

                    product = await this._Product.edit({yearly_price_id: priceModel.id}, {...options, id: product.id});
                }
            } else if (data.stripe_prices) {
                const newPrices = data.stripe_prices.filter(price => !price.stripe_price_id);
                const existingPrices = data.stripe_prices.filter((price) => {
                    return !!price.stripe_price_id && !!price.stripe_product_id;
                });

                for (const existingPrice of existingPrices) {
                    const productId = existingPrice.stripe_product_id;
                    let stripeProduct = await this._StripeProduct.findOne({stripe_product_id: productId}, options);
                    if (!stripeProduct) {
                        stripeProduct = await this._StripeProduct.add({
                            product_id: product.id,
                            stripe_product_id: productId
                        }, options);
                    }
                    const stripePrice = await this._StripePrice.findOne({stripe_price_id: existingPrice.stripe_price_id}, options);

                    if (!stripePrice) {
                        await this._StripePrice.add({
                            stripe_price_id: existingPrice.stripe_price_id,
                            stripe_product_id: stripeProduct.get('stripe_product_id'),
                            active: existingPrice.active,
                            nickname: existingPrice.nickname,
                            description: existingPrice.description,
                            currency: existingPrice.currency,
                            amount: existingPrice.amount,
                            type: existingPrice.type,
                            interval: existingPrice.interval
                        }, options);
                    } else {
                        const updated = await this._StripePrice.edit({
                            nickname: existingPrice.nickname,
                            description: existingPrice.description,
                            active: existingPrice.active
                        }, {
                            ...options,
                            id: stripePrice.id
                        });

                        await this._stripeAPIService.updatePrice(updated.get('stripe_price_id'), {
                            nickname: updated.get('nickname'),
                            active: updated.get('active')
                        });
                    }
                }

                for (const newPrice of newPrices) {
                    const productId = newPrice.stripe_product_id;
                    const stripeProduct = productId ?
                        await this._StripeProduct.findOne({stripe_product_id: productId}, options) : defaultStripeProduct;

                    const price = await this._stripeAPIService.createPrice({
                        product: stripeProduct.get('stripe_product_id'),
                        active: true,
                        nickname: newPrice.nickname,
                        currency: newPrice.currency,
                        amount: newPrice.amount,
                        type: newPrice.type,
                        interval: newPrice.interval
                    });

                    await this._StripePrice.add({
                        stripe_price_id: price.id,
                        stripe_product_id: stripeProduct.get('stripe_product_id'),
                        active: price.active,
                        nickname: price.nickname,
                        description: newPrice.description,
                        currency: price.currency,
                        amount: price.unit_amount,
                        type: price.type,
                        interval: price.recurring && price.recurring.interval || null
                    }, options);
                }
            }

            await product.related('stripePrices').fetch(options);
            await product.related('monthlyPrice').fetch(options);
            await product.related('yearlyPrice').fetch(options);
            await product.related('benefits').fetch(options);
        }

        return product;
    }

    /**
     * Returns a paginated list of Products
     *
     * @params {object} options
     *
     * @returns {Promise<{data: ProductModel[], meta: object}>}
     **/
    async list(options = {}) {
        if (!options.transacting) {
            return this._Product.transaction((transacting) => {
                return this.list({
                    ...options,
                    transacting
                });
            });
        }
        return this._Product.findPage(options);
    }

    async destroy() {
        throw new MethodNotAllowedError('Cannot destroy products, yet...');
    }
}

module.exports = ProductRepository;
<|endoftext|>


<commit_before>Rectangle = Base.extend({
	beans: true,

	initialize: function() {
		if (arguments.length == 1) {
			var rect = arguments[0];
			this.x = rect.x;
			this.y = rect.y;
			this.width = rect.width;
			this.height = rect.height;
		} else if (arguments.length == 2) {
			if (arguments[1].x !== undefined) {
				// new Rectangle(point1, point2)
				var point1 = new Point(arguments[0]);
				var point2 = new Point(arguments[1]);
				this.x = point1.x;
				this.y = point1.y;
				this.width = point2.x - point1.x;
				this.height = point2.y - point1.y;
				if (this.width < 0) {
					this.x = point2.x;
					this.width = -this.width;
				}
				if (this.height < 0) {
					this.y = point2.y;
					this.height = -this.height;
				}
			} else {
				// new Rectangle(point, size)
				var point = new Point(arguments[0]);
				var size = new Size(arguments[1]);
				this.x = point.x;
				this.y = point.y;
				this.width = size.width;
				this.height = size.height;
			}
		} else if (arguments.length == 4) {
			// new Rectangle(x, y, width, height)
			this.x = arguments[0];
			this.y = arguments[1];
			this.width = arguments[2];
			this.height = arguments[3];
		} else {
			// new Rectangle()
			this.x = this.y = this.width = this.height = 0;
		}
	},

	getPoint: function() {
		return new Point(this.x, this.y);
	},

	setPoint: function() {
		var point = Point.read(arguments);
		this.x = point.x;
		this.y = point.y;
	},

	getSize: function() {
		return new Size(this.width, this.height);
	},

	setSize: function() {
		var size = Size.read(arguments);
		this.width = size.width;
		this.height = size.height;
	},

	getLeft: function() {
		return this.x;
	},

	setLeft: function(left) {
		// right should not move
		this.width -= left - this.x;
		this.x = left;
	},

	getTop: function() {
		return this.y;
	},

	setTop: function(top) {
		this.height -= top - this.y;
		this.y = top;
	},

	getRight: function() {
		return this.x + this.width;
	},

	setRight: function(right) {
		this.width = right - this.x;
	},

	getBottom: function() {
		return this.y + this.height;
	},

	setBottom: function(bottom) {
		this.height = bottom - this.y;
	},

	getCenterX: function() {
		return this.x + this.width * 0.5;
	},

	setCenterX: function(x) {
		this.x = x - this.width * 0.5;
	},

	getCenterY: function() {
		return this.y + this.height * 0.5;
	},

	setCenterY: function(y) {
		this.y = y - this.height * 0.5;
	},

	getCenter: function() {
		return new Point(this.x - this.width / 2, this.y - this.height / 2);
	},

	setCenter: function() {
		var center = Point.read(arguments);
		this.x = center.x - this.width / 2;
		this.y = center.y - this.height / 2;
	},

	getTopLeft: function() {
		return new Point(this.left, this.top);
	},

	setTopLeft: function() {
		var topLeft = Point.read(arguments);
		this.left = topLeft.x;
		this.top = topLeft.y;
	},

	getTopRight: function() {
		return new Point(this.right, this.top);
	},

	setTopRight: function() {
		var topRight = Point.read(arguments);
		this.right = topRight.x;
		this.top = topRight.y;
	},

	getBottomLeft: function() {
		return new Point(this.left, this.bottom);
	},

	setBottomLeft: function() {
		var bottomLeft = Point.read(arguments);
		this.left = bottomLeft.x;
		this.bottom = bottomLeft.y;
	},

	getBottomRight: function() {
		return new Point(this.right, this.bottom);
	},

	setBottomRight: function() {
		var bottomRight = Point.read(arguments);
		this.bottom = bottomRight.y;
		this.right = bottomRight.x;
	},

	getLeftCenter: function() {
		return new Point(this.left, this.centerY);
	},

	setLeftCenter: function() {
		var leftCenter = Point.read(arguments);
		this.left = leftCenter.x;
		this.centerY = leftCenter.y;
	},

	getTopCenter: function() {
		return new Point(this.centerX, this.top);
	},

	setTopCenter: function() {
		var topCenter = Point.read(arguments);
		this.centerX = topCenter.x;
		this.top = topCenter.y;
	},

	getRightCenter: function() {
		return new Point(this.right, this.centerY);
	},

	setRightCenter: function() {
		var rightCenter = Point.read(arguments);
		this.right = rightCenter.x;
		this.centerY = rightCenter.y;
	},

	getBottomCenter: function() {
		return new Point(this.centerX, this.bottom);
	},

	setBottomCenter: function() {
		var bottomCenter = Point.read(arguments);
		this.bottom = bottomCenter.y;
		this.centerX = bottomCenter.x;
	},

	clone: function() {
		return new Rectangle(this);
	},

	equals: function() {
		var rect = Rectangle.read(arguments);
		return this.x == rect.x && this.y == rect.y
				&& this.width == rect.width && this.height == rect.height;
	},

	isEmpty: function() {
		return this.width == 0 || this.height == 0;
	},

	contains: function(rect) {
		if (rect.width !== undefined) {
			return rect.x >= this.x && rect.y >= this.y
					&& rect.x + rect.width <= this.x + this.width
					&& rect.y + rect.height <= this.y + this.height;
		} else {
			var point = Point.read(arguments);
			return point.x >= this.x && point.y >= this.y
					&& point.x <= this.x + this.width
					&& point.y <= this.y + this.height;
		}
	},

	intersects: function() {
		var rect = Rectangle.read(arguments);
		return rect.x + rect.width > this.x
				&& rect.y + rect.height > this.y
				&& rect.x < this.x + this.width
				&& rect.y < this.y + this.height;
	},

	intersect: function() {
		var rect = Rectangle.read(arguments);
		var x1 = Math.max(this.x, rect.x);
		var y1 = Math.max(this.y, rect.y);
		var x2 = Math.min(this.x + this.width, rect.x + rect.width);
		var y2 = Math.min(this.y + this.height, rect.y + rect.height);
		return new Rectangle(x1, y1, x2 - x1, y2 - y1);
	},

	unite: function() {
		var rect = Rectangle.read(arguments);
		var x1 = Math.min(this.x, rect.x);
		var y1 = Math.min(this.y, rect.y);
		var x2 = Math.max(this.x + this.width, rect.x + rect.width);
		var y2 = Math.max(this.y + this.height, rect.y + rect.height);
		return new Rectangle(x1, y1, x2 - x1, y2 - y1);
	},

	include: function() {
		var point = Point.read(arguments);
		var x1 = Math.min(this.x, point.x);
		var y1 = Math.min(this.y, point.y);
		var x2 = Math.max(this.x + this.width, point.x);
		var y2 = Math.max(this.y + this.height, point.y);
		return new Rectangle(x1, y1, x2 - x1, y2 - y1);
	},

	toString: function() {
		return '{ x: ' + this.x
				+ ', y: ' + this.y
				+ ', width: ' + this.width
				+ ', height: ' + this.height
				+ ' }';
	},

	statics: {
		read: function(args, index) {
			var index = index || 0, length = args.length - index;
			if (length == 1 && args[index] instanceof Rectangle) {
				return args[index];
			} else if (length != 0) {
				var rect = new Rectangle();
				rect.initialize.apply(rect, index > 0
						? Array.prototype.slice.call(args, index) : args);
				return rect;
			}
			return null;
		}
	}
});
<commit_msg>Fix Rectangle#center (use + instead of -).<commit_after>Rectangle = Base.extend({
	beans: true,

	initialize: function() {
		if (arguments.length == 1) {
			var rect = arguments[0];
			this.x = rect.x;
			this.y = rect.y;
			this.width = rect.width;
			this.height = rect.height;
		} else if (arguments.length == 2) {
			if (arguments[1].x !== undefined) {
				// new Rectangle(point1, point2)
				var point1 = new Point(arguments[0]);
				var point2 = new Point(arguments[1]);
				this.x = point1.x;
				this.y = point1.y;
				this.width = point2.x - point1.x;
				this.height = point2.y - point1.y;
				if (this.width < 0) {
					this.x = point2.x;
					this.width = -this.width;
				}
				if (this.height < 0) {
					this.y = point2.y;
					this.height = -this.height;
				}
			} else {
				// new Rectangle(point, size)
				var point = new Point(arguments[0]);
				var size = new Size(arguments[1]);
				this.x = point.x;
				this.y = point.y;
				this.width = size.width;
				this.height = size.height;
			}
		} else if (arguments.length == 4) {
			// new Rectangle(x, y, width, height)
			this.x = arguments[0];
			this.y = arguments[1];
			this.width = arguments[2];
			this.height = arguments[3];
		} else {
			// new Rectangle()
			this.x = this.y = this.width = this.height = 0;
		}
	},

	getPoint: function() {
		return new Point(this.x, this.y);
	},

	setPoint: function() {
		var point = Point.read(arguments);
		this.x = point.x;
		this.y = point.y;
	},

	getSize: function() {
		return new Size(this.width, this.height);
	},

	setSize: function() {
		var size = Size.read(arguments);
		this.width = size.width;
		this.height = size.height;
	},

	getLeft: function() {
		return this.x;
	},

	setLeft: function(left) {
		// right should not move
		this.width -= left - this.x;
		this.x = left;
	},

	getTop: function() {
		return this.y;
	},

	setTop: function(top) {
		this.height -= top - this.y;
		this.y = top;
	},

	getRight: function() {
		return this.x + this.width;
	},

	setRight: function(right) {
		this.width = right - this.x;
	},

	getBottom: function() {
		return this.y + this.height;
	},

	setBottom: function(bottom) {
		this.height = bottom - this.y;
	},

	getCenterX: function() {
		return this.x + this.width * 0.5;
	},

	setCenterX: function(x) {
		this.x = x - this.width * 0.5;
	},

	getCenterY: function() {
		return this.y + this.height * 0.5;
	},

	setCenterY: function(y) {
		this.y = y - this.height * 0.5;
	},

	getCenter: function() {
		return new Point(this.x + this.width * 0.5, this.y + this.height * 0.5);
	},

	setCenter: function() {
		var center = Point.read(arguments);
		this.x = center.x - this.width * 0.5;
		this.y = center.y - this.height * 0.5;
	},

	getTopLeft: function() {
		return new Point(this.left, this.top);
	},

	setTopLeft: function() {
		var topLeft = Point.read(arguments);
		this.left = topLeft.x;
		this.top = topLeft.y;
	},

	getTopRight: function() {
		return new Point(this.right, this.top);
	},

	setTopRight: function() {
		var topRight = Point.read(arguments);
		this.right = topRight.x;
		this.top = topRight.y;
	},

	getBottomLeft: function() {
		return new Point(this.left, this.bottom);
	},

	setBottomLeft: function() {
		var bottomLeft = Point.read(arguments);
		this.left = bottomLeft.x;
		this.bottom = bottomLeft.y;
	},

	getBottomRight: function() {
		return new Point(this.right, this.bottom);
	},

	setBottomRight: function() {
		var bottomRight = Point.read(arguments);
		this.bottom = bottomRight.y;
		this.right = bottomRight.x;
	},

	getLeftCenter: function() {
		return new Point(this.left, this.centerY);
	},

	setLeftCenter: function() {
		var leftCenter = Point.read(arguments);
		this.left = leftCenter.x;
		this.centerY = leftCenter.y;
	},

	getTopCenter: function() {
		return new Point(this.centerX, this.top);
	},

	setTopCenter: function() {
		var topCenter = Point.read(arguments);
		this.centerX = topCenter.x;
		this.top = topCenter.y;
	},

	getRightCenter: function() {
		return new Point(this.right, this.centerY);
	},

	setRightCenter: function() {
		var rightCenter = Point.read(arguments);
		this.right = rightCenter.x;
		this.centerY = rightCenter.y;
	},

	getBottomCenter: function() {
		return new Point(this.centerX, this.bottom);
	},

	setBottomCenter: function() {
		var bottomCenter = Point.read(arguments);
		this.bottom = bottomCenter.y;
		this.centerX = bottomCenter.x;
	},

	clone: function() {
		return new Rectangle(this);
	},

	equals: function() {
		var rect = Rectangle.read(arguments);
		return this.x == rect.x && this.y == rect.y
				&& this.width == rect.width && this.height == rect.height;
	},

	isEmpty: function() {
		return this.width == 0 || this.height == 0;
	},

	contains: function(rect) {
		if (rect.width !== undefined) {
			return rect.x >= this.x && rect.y >= this.y
					&& rect.x + rect.width <= this.x + this.width
					&& rect.y + rect.height <= this.y + this.height;
		} else {
			var point = Point.read(arguments);
			return point.x >= this.x && point.y >= this.y
					&& point.x <= this.x + this.width
					&& point.y <= this.y + this.height;
		}
	},

	intersects: function() {
		var rect = Rectangle.read(arguments);
		return rect.x + rect.width > this.x
				&& rect.y + rect.height > this.y
				&& rect.x < this.x + this.width
				&& rect.y < this.y + this.height;
	},

	intersect: function() {
		var rect = Rectangle.read(arguments);
		var x1 = Math.max(this.x, rect.x);
		var y1 = Math.max(this.y, rect.y);
		var x2 = Math.min(this.x + this.width, rect.x + rect.width);
		var y2 = Math.min(this.y + this.height, rect.y + rect.height);
		return new Rectangle(x1, y1, x2 - x1, y2 - y1);
	},

	unite: function() {
		var rect = Rectangle.read(arguments);
		var x1 = Math.min(this.x, rect.x);
		var y1 = Math.min(this.y, rect.y);
		var x2 = Math.max(this.x + this.width, rect.x + rect.width);
		var y2 = Math.max(this.y + this.height, rect.y + rect.height);
		return new Rectangle(x1, y1, x2 - x1, y2 - y1);
	},

	include: function() {
		var point = Point.read(arguments);
		var x1 = Math.min(this.x, point.x);
		var y1 = Math.min(this.y, point.y);
		var x2 = Math.max(this.x + this.width, point.x);
		var y2 = Math.max(this.y + this.height, point.y);
		return new Rectangle(x1, y1, x2 - x1, y2 - y1);
	},

	toString: function() {
		return '{ x: ' + this.x
				+ ', y: ' + this.y
				+ ', width: ' + this.width
				+ ', height: ' + this.height
				+ ' }';
	},

	statics: {
		read: function(args, index) {
			var index = index || 0, length = args.length - index;
			if (length == 1 && args[index] instanceof Rectangle) {
				return args[index];
			} else if (length != 0) {
				var rect = new Rectangle();
				rect.initialize.apply(rect, index > 0
						? Array.prototype.slice.call(args, index) : args);
				return rect;
			}
			return null;
		}
	}
});
<|endoftext|>


<commit_before>/**
 *
 * Creates igo-diagrams. Note: This is only for creating books using latex.
 */
gpub.diagrams.igo = {
  /** Font sizes supported by igo. */
  fontSize: {
    5: 5,
    6: 6,
    7: 7,
    8: 8,
    9: 9,
    10: 10,
    11: 11,
    12: 12,
    15: 15,
    20: 20
  },

  /**
   * Create a diagram from a flattened representation.
   *
   * Unlike many other diagram-generators, Igo has lots of built-in logic in the
   * TEX style. Thus, we need only display the stones and marks.
   *
   * Also note: For Igo, Go boards are indexed from the bottom as coordinates of
   * the form <letter><number>:
   *  a12
   *  g5
   * - Numbers are 1 indexed
   * - i is not used for intersections
   *
   * So, the boards look like this:
   *   ...
   *   ^
   *   2
   *   ^
   *   1
   *     a->b->c->d->e->f->g->h->j->k->l...
   */
  create: function(flattened, options) {
    var size = options.size;
    var letters = 'abcdefghjklmnopqrstuvwxyz';
    var symbolStr = glift.flattener.symbolStr;
    var wInts = [];
    var bInts = [];

    var size = flattened.board.maxBoardSize();
    var toPt = glift.util.pointFromString;
    var toIgoCoord  = function(pt) {
      var x = letters.charAt(pt.x());
      var y = size - pty.y();
      return x+y;
    }

    var seenPts = {};
    var stoneMap = flattened.stoneMap();

    var procMarks = {
      BLACK: {
        XMARK: [],
        SQUARE: [],
        TRIANGLE: [],
        CIRCLE: []
      },
      WHITE: {
        XMARK: [],
        SQUARE: [],
        TRIANGLE: [],
        CIRCLE: []
      }
    };

    var markToGlyph = {
      XMARK: '\\igocross',
      SQUARE: '\\igosquare',
      TRIANGLE: '\\igotriangle',
      CIRCLE: '\\igocircle'
    };

    for (var ptstr in flattened.markMap()) {
      var mark = flattened.markMap()[ptstr];
      // Note: Igo does *not* support marks (except for text) on empty
      // intersections, so we must makes user that there are intersections on
      if (stoneMap[ptstr] &&
          stoneMap[ptstr].color &&
          mark != glift.flattened.symbols.TEXTLABEL) {
        var stone = stoneMap[ptstr];
        procMarks[stone.color][symbolStr(mark)] = toIgoCoord(toPt(ptstr));
        seenPts[ptstr] = true;
      } else if (mark != glift.flattened.symbols.TEXTLABEL) {
        // Handle separately
      }
    }

    for (var ptstr in flattened.stoneMap()) {
      if (proc[ptstr]) {
        // We already processed this stone during the mark-phase
        continue;
      }
      var stone = flattened.stoneMap()[ptstr];
      if (stone.color === glift.enums.states.WHITE) {
        wInts.push(toIgoCoord(toPt(ptstr)));
      } else if (stone.color === glift.enums.states.BLACK) {
        bInts.push(toIgoCoord(toPt(ptstr)));
      } else {
        // Somehow, there's no color for the move. Which is strange.
      }
    }

    var out = [
      '\\cleargoban',
    ]

    return out.join('\n');
  },
  /**
   * Render go stones that exist in a block of text.
   */
  renderInline: function(text, options) {
    // TODO(kashomon): Implement at some point. See gnos for an example.
    return text;
  }
};
<commit_msg>More work on igo diagram generation.<commit_after>/**
 *
 * Creates igo-diagrams. Note: This is only for creating books using latex.
 */
gpub.diagrams.igo = {
  /** Font sizes supported by igo. */
  fontSize: {
    5: 5,
    6: 6,
    7: 7,
    8: 8,
    9: 9,
    10: 10,
    11: 11,
    12: 12,
    15: 15,
    20: 20
  },

  /**
   * Create a diagram from a flattened representation.
   *
   * Unlike many other diagram-generators, Igo has lots of built-in logic in the
   * TEX style. Thus, we need only display the stones and marks.
   *
   * Also note: For Igo, Go boards are indexed from the bottom as coordinates of
   * the form <letter><number>:
   *  a12
   *  g5
   * - Numbers are 1 indexed
   * - i is not used for intersections
   *
   * So, the boards look like this:
   *   ...
   *   ^
   *   2
   *   ^
   *   1
   *     a->b->c->d->e->f->g->h->j->k->l...
   */
  create: function(flattened, options) {
    var size = options.size;
    var letters = 'abcdefghjklmnopqrstuvwxyz';
    var symbolStr = glift.flattener.symbolStr;
    var wInts = [];
    var bInts = [];

    var size = flattened.board.maxBoardSize();
    var toPt = glift.util.pointFromString;
    var toIgoCoord  = function(pt) {
      var x = letters.charAt(pt.x());
      var y = size - pty.y();
      return x+y;
    }

    var seenPts = {};
    var stoneMap = flattened.stoneMap();

    var procMarks = {
      BLACK: {
        // XMARK->CIRCLE are arrays of pt strings
        XMARK: [],
        SQUARE: [],
        TRIANGLE: [],
        CIRCLE: [],
        // TEXTLABEL is an array of objects like {ptstr: str, label: str}
        TEXTLABEL: []
      },
      WHITE: {
        XMARK: [],
        SQUARE: [],
        TRIANGLE: [],
        CIRCLE: [],
        TEXTLABEL: []
      },
      EMPTY: {
        TEXTLABEL: []
      }
    };

    // Glyphs are used in the context: \white|black[glyph]{intersection-pairs}
    var markToGlyph = {
      XMARK: '\\igocross',
      SQUARE: '\\igosquare',
      TRIANGLE: '\\igotriangle',
      CIRCLE: '\\igocircle'
    };

    var number = /^\d+$/;
    for (var ptstr in flattened.markMap()) {
      var mark = flattened.markMap()[ptstr];
      if (stoneMap[ptstr] && stoneMap[ptstr].color) {
        // Note: Igo does not support marks (except for number-labels) on
        // empty intersections.
        if (mark !== glift.flattened.symbols.TEXTLABEL) {
          var stone = stoneMap[ptstr];
          procMarks[stone.color][symbolStr(mark)] = toIgoCoord(toPt(ptstr));
          seenPts[ptstr] = true;
        } else if (mark === glift.flattened.symbols.TEXTLABEL &&
            flattened.labelMap()[ptstr] &&
            number.test(flattened.labelMap()[ptstr])) {
          // Only number-labels are support on stones.
          // TODO(kashomon): Since Igo supports special constructions for
          // consecutive stones, this constructions hould be supported here
          procMarks[stone.color][symbolStr(mark)] = {
            ptstr: ptstr,
            label: flattened.labelMap()[ptstr]
          };
        }
      } else if (mark == glift.flattened.symbols.TEXTLABEL) {
        // Arbitrary labels are supported for empty intersections.
        proc[EMPTY][TEXTLABEL] =  {
          ptstr: ptstr,
          label: flattened.labelMap()[ptstr]
        };
      }
      // There are several opportunities for marks to not get caught here. Igo
      // doesn't have support for:
      // - Empty intersections with marks
      // - Stones with labels other than numbers
    }

    for (var ptstr in flattened.stoneMap()) {
      if (proc[ptstr]) {
        // We already processed this stone during the mark-phase
        continue;
      }
      var stone = flattened.stoneMap()[ptstr];
      if (stone.color === glift.enums.states.WHITE) {
        wInts.push(toIgoCoord(toPt(ptstr)));
      } else if (stone.color === glift.enums.states.BLACK) {
        bInts.push(toIgoCoord(toPt(ptstr)));
      } else {
        // Somehow, there's no color for the move. Which is strange.
      }
    }

    var out = [
      '\\cleargoban',
    ]

    return out.join('\n');
  },
  /**
   * Render go stones that exist in a block of text.
   */
  renderInline: function(text, options) {
    // TODO(kashomon): Implement at some point. See gnos for an example.
    return text;
  }
};
<|endoftext|>


<commit_before>import _ from 'underscore';
import metadataEvaluator from '../lib/MetadataEvaluator.js';
import metadataProvider from '../lib/metadataProvider.js';
import dataEvaluator from '../lib/DataEvaluator.js';
import collectionHelper from './../lib/helpers/collectionHelper.js';
import objectHelper from '../lib/helpers/objectHelper.js';
import typeProcessorFactory from '../lib/typeProcessorFactory.js';

class MetaFormStateManager {

    /**
     *
     * @param schema
     * @param entityName
     * @param layoutName
     * @param model
     * @param stateGetter
     * @param stateSetter
     */
    constructor(schema, entityName, layoutName, model, stateGetter, stateSetter) {
        this.schema = schema;
        this.entityName = entityName;
        this.layoutName = layoutName;
        this.model = model;
        this.stateGetter = stateGetter;
        this.stateSetter = stateSetter;
    }

    getState() {
        return this.stateGetter();
    }

    setState(state) {
        this.stateSetter(state);
    }

    /**
     * Gets the initial state
     * @returns {{validationSummary: {open: boolean, messages: (*|Array)}, fields: *, entity: entity, layout: layout, model: *, componentProps: Object}}
     */
    getInitialState() {
        let entityAndLayout = metadataProvider.getEntityAndLayout(this.schema, this.entityName, this.layoutName);
        let fields = metadataProvider.getFields(this.schema, entityAndLayout.entity, entityAndLayout.layout);
        let componentProps = this.getComponentProps(fields, this.model);

        return {
            validationSummary: {
                open: false,
                messages: MetaFormStateManager.getValidationSummaryMessages(componentProps)
            },
            fields: fields,
            entity: entityAndLayout.entity,
            layout: entityAndLayout.layout,
            model: this.model,
            // object with a key for each property
            componentProps: componentProps
        }
    }

    /**
     * Updates de model and the componentProps for the new value
     * @param fieldMetadata
     * @param newValue
     */
    updateState(fieldMetadata, newValue) {
        let newState = _.extend({}, this.getState());

        let typeProcessorType = typeProcessorFactory.getProcessorType(fieldMetadata.type);
        let typeProcessor = new typeProcessorType();
        let typeProcessed = typeProcessor.process(newValue);

        let modelFieldKey = fieldMetadata.key;
        let componentPropsFieldKey = modelFieldKey.indexOf('.') == -1 ? modelFieldKey : modelFieldKey.split('.').join('.componentProps.');

        if (typeProcessed.valid) {
            // the user input is valid for it's type

            // update the model
            objectHelper.setValue(newState.model, modelFieldKey, typeProcessed.convertedValue);

            // recalculate the componentProps for all components
            newState.componentProps = this.getComponentProps(newState.fields, newState.model);

            // set the raw value for the modified component
            objectHelper.setValue(newState.componentProps, `${componentPropsFieldKey}.rawValue`, newValue);

            // set the validation messages
            newState.validationSummary.messages = MetaFormStateManager.getValidationSummaryMessages(newState.componentProps);
        }
        else {
            // the user input is not valid for it's type.
            // in this case, there's no need to update the model neither to reprocess all
            // the componentProps
            objectHelper.setValue(newState.componentProps, `${componentPropsFieldKey}.rawValue`, newValue);
            objectHelper.setValue(newState.componentProps, `${componentPropsFieldKey}.invalid`, {
                value: true,
                message: `The field '${componentPropsFieldKey}' should be a valid ${fieldMetadata.type}.`
            });
            newState.validationSummary.messages = MetaFormStateManager.getValidationSummaryMessages(newState.componentProps);
        }

        this.setState(newState);
    }

    /**
     * Returns a validation summary for the given componentProps
     * @param componentProps
     * @returns {Array}
     */
    static getValidationSummaryMessages(componentProps) {
        let result = [];
        for (let key in componentProps) {
            if (componentProps.hasOwnProperty(key)) {
                if (componentProps[key].invalid && componentProps[key].invalid.value == true) {
                    result.push(componentProps[key].invalid.message);
                }
            }
        }
        return result;
    }

    /**
     * Returns an object with a property for each given field metadata. The value is the metadata already
     * processed for the given model
     * @param fields
     * @param model
     * @returns {Object}
     * @private
     */
    getComponentProps(fields, model) {
        // will evaluate all the fields and return an array.
        // evaluation means converting every function, for instance, in actual values.
        let processedFields = metadataEvaluator.evaluate(fields, model);
        let processField = null;
        let _this = this;

        processField = (field, prefix) => {
            field.onChange = e => _this.updateState(field, e.value);

            if (field.type == 'entity') {
                let newPrefix = prefix ? `${prefix}.${field.name}` : field.name;
                field.componentProps = collectionHelper.toObject(field.fields.map(f => processField(f, newPrefix)));
            }
            else if (field.type == 'array') {

            }
            return field;
        };

        // the component props are basically the matadata found within the 'fields' array,
        // with some added properties. Let's add these properties.
        processedFields = processedFields.map(f => processField(f));

        // will convert the array into an object
        return collectionHelper.toObject(processedFields, 'name');
    }

    /**
     * Updates de componentProps
     * @param model
     */
    updateComponentProps(model) {
        let newState = _.extend({}, this.getState());
        newState.componentProps = this.getComponentProps(newState.fields, model);
        this.setState(newState);
    }
}

export default MetaFormStateManager;<commit_msg>Complete refactoring on the MetaFormStateManager<commit_after>import _ from 'underscore';
import metadataEvaluator from '../lib/MetadataEvaluator.js';
import metadataProvider from '../lib/metadataProvider.js';
import dataEvaluator from '../lib/DataEvaluator.js';
import collectionHelper from './../lib/helpers/collectionHelper.js';
import objectHelper from '../lib/helpers/objectHelper.js';
import typeProcessorFactory from '../lib/typeProcessorFactory.js';

class MetaFormStateManager {

    /**
     *
     * @param schema
     * @param entityName
     * @param layoutName
     * @param model
     * @param stateGetter
     * @param stateSetter
     */
    constructor(schema, entityName, layoutName, model, stateGetter, stateSetter) {
        this.schema = schema;
        this.entityName = entityName;
        this.layoutName = layoutName;
        this.model = model;
        this.stateGetter = stateGetter;
        this.stateSetter = stateSetter;
        this.metadataIndex = {};

        this.entityAndLayout = metadataProvider.getEntityAndLayout(this.schema, this.entityName, this.layoutName);
        this.fields = metadataProvider.getFields(this.schema, this.entityAndLayout.entity, this.entityAndLayout.layout);
    }

    getState() {
        return this.stateGetter();
    }

    setState(state) {
        this.stateSetter(state);
    }

    /**
     * Gets the initial state
     * @returns {{validationSummary: {open: boolean, messages: (*|Array)}, fields: *, entity: entity, layout: layout, model: *, componentProps: Object}}
     */
    getInitialState() {
        return {
            validationSummary: {
                open: false,
                messages: MetaFormStateManager.getValidationSummaryMessages()
            },
            entity: this.entityAndLayout.entity, // this seems useless
            layout: this.entityAndLayout.layout, // this seems useless
            model: this.model,
            // object with a key for each property
            componentProps: this.getComponentProps(this.fields, this.model)
        }
    }

    /**
     * Updates de model and the componentProps for the new value
     * @param key
     * @param newValue
     */
    updateState(key, newValue) {
        let newState = _.extend({}, this.getState());
        let fieldMetadata = this.metadataIndex[key];
        if(!fieldMetadata) {
            throw Error(`could not find metadata for the given field. Field: ${key}`);
        }
        let typeProcessorType = typeProcessorFactory.getProcessorType(fieldMetadata.type);
        let typeProcessor = new typeProcessorType();
        let typeProcessed = typeProcessor.process(newValue);

        //let modelFieldKey = fieldMetadata.key;
        //let componentPropsFieldKey = modelFieldKey.indexOf('.') == -1 ? modelFieldKey : modelFieldKey.split('.').join('.componentProps.');

        if (typeProcessed.valid) {
            // the user input is valid for it's type

            // update the model
            objectHelper.setValue(newState.model, key, typeProcessed.convertedValue);

            // recalculate the componentProps for all components
            newState.componentProps = this.getComponentProps(newState.fields, newState.model);
        }
        else {
            // the user input is not valid for it's type.
            // in this case, there's no need to update the model neither to reprocess all
            // the componentProps
            fieldMetadata.invalid = {
                value: true,
                message: `The field '${key}' should be a valid ${fieldMetadata.type}.`
            };
        }
        // set the raw value for the modified component
        fieldMetadata.rawValue = newValue;
        // set the validation messages
        newState.validationSummary.messages = MetaFormStateManager.getValidationSummaryMessages();

        this.setState(newState);
    }

    /**
     * Returns a validation summary for the given componentProps
     * @param componentProps
     * @returns {Array}
     */
    static getValidationSummaryMessages() {

        let result = [];
        for (let key in this.metadataIndex) {
            if (this.metadataIndex.hasOwnProperty(key)) {
                if (this.metadataIndex[key].invalid && this.metadataIndex[key].invalid.value == true) {
                    result.push(this.metadataIndex[key].invalid.message);
                }
            }
        }
        return result;
    }

    /**
     * Returns an object with a property for each given field metadata. The value is the metadata already
     * processed for the given model
     * @param fields
     * @param model
     * @returns {Object}
     * @private
     */
    getComponentProps(fields, model) {
        return metadataEvaluator.evaluate(fields, model,'', this.metadataIndex, (k,v) => this.updateState(k, v));
    }

    /**
     * Updates de componentProps
     * @param model
     */
    updateComponentProps(model) {
        let newState = _.extend({}, this.getState());
        newState.componentProps = this.getComponentProps(this.fields, model);
        this.setState(newState);
    }
}

export default MetaFormStateManager;<|endoftext|>


<commit_before>const localStorageMock = () => {
  const storage = {}
  return {
    getItem(key) {
      return storage[key]
    },
    setItem(key, value) {
      storage[key] = value.toString()
    },
    // clear() {
    //   storage = {}
    // },
    removeItem(key) {
      delete storage[key]
    }
  }
}

const createLocalStorageMock = () => {
  Object.defineProperty(global, 'localStorage', { value: localStorageMock() })
}

export default createLocalStorageMock
<commit_msg>fix failing tests with localStorage<commit_after>const localStorageMock = () => {
  const storage = {}
  return {
    getItem(key) {
      return storage[key]
    },
    setItem(key, value) {
      storage[key] = value.toString()
    },
    // clear() {
    //   storage = {}
    // },
    removeItem(key) {
      delete storage[key]
    }
  }
}

const createLocalStorageMock = () => {
  global.localStorage = localStorageMock()
}

export default createLocalStorageMock
<|endoftext|>


<commit_before>var es = require('event-stream');
var gulp = require('gulp');
var mox = require('./mock-knox');

describe('publisher', function () {
	var publisher,
		gulpS3,
		mockedStream;

	// replace all the required module with either mocked module or
	// the exact same module so that istanbul would not include them in the coverage
	beforeEach(function() {
		mockedStream = es.mapSync( function (file) { return file;});
		gulpS3 = sinon.stub().returns( mockedStream );
		publisher = SandboxedModule.require('../publisher', {
			requires: {
				'gulp-s3': gulpS3,
				'event-stream': es,
				'knox': mox
			}
		});
	});

	describe ('Parameter', function () {
		it('should throw with null options', function() {
			expect(publisher).to.throw( 'Missing options' );
		});

		it('should throw with empty options', function() {
			expect(function() {
				publisher({});
			}).to.throw( 'Missing app id' );
		});

		it('should throw with no credentials', function() {
			var options = {
				appID: 'some-ID',
				devTag: 'some-tag'
			}
			expect(function() {
				publisher(options);
			}).to.throw( 'Missing credentials' );
		});

		it('should throw with no key', function() {
			var options = {
				appID: 'some-ID',
				creds: {},
				devTag: 'some-tag'
			}
			expect(function() {
				publisher(options);
			}).to.throw( 'Missing credential key' );
		});

		it('should throw with no secret', function() {
			var options = {
				appID: 'some-ID',
				creds: {
					key: 'some-key'
				},
				devTag: 'some-tag'
			};

			expect(function() {
				publisher(options);
			}).to.throw( 'Missing credential secret' );
		});

		it('should throw with no devTag', function() {
			var options = {
				appID: 'some-ID',
				creds: {
					key: 'some-key',
					secret: 'some-secret'
				}
			};

			expect(function() {
				publisher(options);
			}).to.throw( 'Missing devTag' );
		});

		it('should throw with no appID', function() {
			var options = {
				creds: {
					key: 'some-key',
					secret: 'some-secret'
				},
				devTag: 'some-tag'
			};
			expect(function() {
				publisher(options);
			}).to.throw( 'Missing app id' );
		});

		it ('should not throw even if there is extra info in the creds', function() {
			var options = {
				appID: 'some-ID',
				creds: {
					key: 'some-key',
					secret: 'some-secret',
					useless: 'testetetse'
				},
				devTag: 'some-tag'
			};

			expect(function() {
				publisher(options);
			}).to.not.throw();
		});
	});

	describe('location', function () {
		it('should return the proper address', function () {
			var options = {
				appID: 'some-ID',
				creds: { key: 'some-key', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			expect(publisher( options ).location).to.equal('https://d2660orkic02xl.cloudfront.net/apps/some-ID/dev/some-tag/');
		});
	});

	describe('stream', function () {

		it('should pipe stream into a s3 bucket with content but not change the file content', function (done) {
			var options = {
				appID: 'some-ID',
				creds: { key: 'key-a', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			var hasError;
			gulp.src('./test/dist/**')
				.pipe( publisher(options) )
				.on('error', function (err) {
					hasError = true;
					done();
					expect(hasError).to.be.false;

				})
				.on('end', function (err) {
					hasError = false;
					done();
					expect(hasError).to.be.false;
				});
		});

		it('should pipe stream into an empty s3 bucket and change the file content', function (done) {
			var options = {
				appID: 'some-ID',
				creds: { key: 'some-key', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			var hasError;
			gulp.src('./test/dist/**')
				.pipe( publisher(options) )
				.on('error', function (err) {
					hasError = true;
					done();
					expect(hasError).to.be.false;

				})
				.on('end', function (err) {
					hasError = false;
					done();
					expect(hasError).to.be.false;
				});
		});

		it('should expect an error when give a wrong key', function (done) {
			var options = {
				appID: 'some-ID',
				creds: { key: 'wrong-key', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			var hasError;
			gulp.src('./test/dist/**')
				.pipe( publisher(options) )
				.on('error', function (err) {
					hasError = true;
					done();
					expect(hasError).to.be.true;

				})
				.on('end', function (err) {
					hasError = false;
					done();
					expect(hasError).to.be.true;
				});

		});
	});

});
<commit_msg>Change the test cases to check for data passing into the gulp-s3 stub<commit_after>var es = require('event-stream');
var gulp = require('gulp');
var mox = require('./mock-knox');

describe('publisher', function () {
	var publisher,
		gulpS3,
		mockedStream;

	// replace all the required module with either mocked module or
	// the exact same module so that istanbul would not include them in the coverage
	beforeEach(function() {
		mockedStream = es.mapSync( function (file) { return file;});
		gulpS3 = sinon.stub().returns( mockedStream );
		publisher = SandboxedModule.require('../publisher', {
			requires: {
				'gulp-s3': gulpS3,
				'event-stream': es,
				'knox': mox
			}
		});
	});

	describe ('Parameter', function () {
		it('should throw with null options', function() {
			expect(publisher).to.throw( 'Missing options' );
		});

		it('should throw with empty options', function() {
			expect(function() {
				publisher({});
			}).to.throw( 'Missing app id' );
		});

		it('should throw with no credentials', function() {
			var options = {
				appID: 'some-ID',
				devTag: 'some-tag'
			}
			expect(function() {
				publisher(options);
			}).to.throw( 'Missing credentials' );
		});

		it('should throw with no key', function() {
			var options = {
				appID: 'some-ID',
				creds: {},
				devTag: 'some-tag'
			}
			expect(function() {
				publisher(options);
			}).to.throw( 'Missing credential key' );
		});

		it('should throw with no secret', function() {
			var options = {
				appID: 'some-ID',
				creds: {
					key: 'some-key'
				},
				devTag: 'some-tag'
			};

			expect(function() {
				publisher(options);
			}).to.throw( 'Missing credential secret' );
		});

		it('should throw with no devTag', function() {
			var options = {
				appID: 'some-ID',
				creds: {
					key: 'some-key',
					secret: 'some-secret'
				}
			};

			expect(function() {
				publisher(options);
			}).to.throw( 'Missing devTag' );
		});

		it('should throw with no appID', function() {
			var options = {
				creds: {
					key: 'some-key',
					secret: 'some-secret'
				},
				devTag: 'some-tag'
			};
			expect(function() {
				publisher(options);
			}).to.throw( 'Missing app id' );
		});

		it ('should not throw even if there is extra info in the creds', function() {
			var options = {
				appID: 'some-ID',
				creds: {
					key: 'some-key',
					secret: 'some-secret',
					useless: 'testetetse'
				},
				devTag: 'some-tag'
			};

			expect(function() {
				publisher(options);
			}).to.not.throw();
		});
	});

	describe('location', function () {
		it('should return the proper address', function () {
			var options = {
				appID: 'some-ID',
				creds: { key: 'some-key', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			expect(publisher( options ).location).to.equal('https://d2660orkic02xl.cloudfront.net/apps/some-ID/dev/some-tag/');
		});
	});

	describe('stream', function () {

		it('should pipe stream into a s3 bucket with content but not change the file content', function (done) {
			var options = {
				appID: 'some-ID',
				creds: { key: 'key-a', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			var passedData = false;
			gulp.src('./test/dist/**')
				.pipe( publisher(options) )
				.on('data', function (data) {

					passedData = true;

				})
				.on('end', function (err) {

					expect(passedData).to.be.false;
					done();
				});
		});

		it('should pipe stream into an empty s3 bucket and change the file content', function (done) {
			var options = {
				appID: 'some-ID',
				creds: { key: 'some-key', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			var passedData = false;
			gulp.src('./test/dist/**')
				.pipe( publisher(options) )
				.on('data', function (data) {

					passedData = true;

				})
				.on('end', function (err) {
					expect(passedData).to.be.true;
					done();
				});
		});

		it('should expect an error when give a wrong key', function (done) {
			var options = {
				appID: 'some-ID',
				creds: { key: 'wrong-key', secret: 'some-secret' },
				devTag: 'some-tag'
			};

			var hasError;
			gulp.src('./test/dist/**')
				.pipe( publisher(options) )
				.on('error', function (err) {

					hasError = true;
					expect(hasError).to.be.true;
					done();

				});

		});
	});

});
<|endoftext|>


<commit_before>require('mocha');
const { IncomingWebhook } = require('./IncomingWebhook');
const { ErrorCode } = require('./errors');
const { assert } = require('chai');
const isPromise = require('p-is-promise');
const nock = require('nock');

const url = 'https://hooks.slack.com/services/FAKEWEBHOOK';

describe('IncomingWebhook', function () {

  describe('constructor()', function () {
    it('should build a default wehbook given a URL', function () {
      const webhook = new IncomingWebhook(url);
      assert.instanceOf(webhook, IncomingWebhook);
    });
  });

  describe('send()', function () {
    beforeEach(function () {
      this.webhook = new IncomingWebhook(url);
    });

    describe('when making a successful call', function () {
      beforeEach(function () {
        this.scope = nock('https://hooks.slack.com')
          .post(/services/)
          .reply(200, 'ok');
      });

      it('should return results in a Promise', function () {
        const result = this.webhook.send('Hello');
        assert(isPromise(result));
        return result.then((result) => {
          assert.strictEqual(result.text, 'ok');
          this.scope.done();
        });
      });

      it('should deliver results in a callback', function (done) {
        this.webhook.send('Hello', (error, result) => {
          assert.notOk(error);
          assert.strictEqual(result.text, 'ok');
          this.scope.done();
          done();
        });
      });
    });

    describe('when the call fails', function () {
      beforeEach(function () {
        this.scope = nock('https://hooks.slack.com')
          .post(/services/)
          .reply(500);
      });

      it('should return a Promise which rejects on error', function (done) {
        const result = this.webhook.send('Hello');
        assert(isPromise(result));
        result.catch((error) => {
          assert.ok(error);
          assert.instanceOf(error, Error);
          done();
        });
      });

      it('should deliver the error in a callback', function (done) {
        this.webhook.send('Hello', (error) => {
          assert.ok(error);
          assert.instanceOf(error, Error);
          done();
        });
      });

      it('should fail with IncomingWebhookRequestError when the API request fails', function (done) {
        // One known request error is when the node encounters an ECONNREFUSED. In order to simulate this, rather than
        // using nock, we send the request to a host:port that is not listening.
        const webhook = new IncomingWebhook('https://localhost:8999/api/');
        webhook.send('Hello', (error) => {
          assert.instanceOf(error, Error);
          assert.equal(error.code, ErrorCode.IncomingWebhookRequestError);
          assert.instanceOf(error.original, Error);
          done();
        });
      });
    });

    describe('lifecycle', function () {
      beforeEach(function () {
        this.scope = nock('https://hooks.slack.com')
          .post(/services/)
          .reply(500);
      });

      it('should not overwrite the default parameters after a call', function (done) {
        const defaultParams  = { channel: 'default' };
        const expectedParams = Object.assign({}, defaultParams);
        const webhook        = new IncomingWebhook(url, defaultParams);

        webhook.send({ channel: 'different' }, () => {
          assert.deepEqual(webhook.defaults, expectedParams);
          done();
        });
      });
    });

  });
});
<commit_msg>Modify Incoming webhook spec for custom agent param<commit_after>require('mocha');
const { Agent } = require('https');
const { IncomingWebhook } = require('./IncomingWebhook');
const { ErrorCode } = require('./errors');
const { assert } = require('chai');
const isPromise = require('p-is-promise');
const nock = require('nock');
const sinon = require('sinon');

const url = 'https://hooks.slack.com/services/FAKEWEBHOOK';

describe('IncomingWebhook', function () {

  describe('constructor()', function () {
    it('should build a default wehbook given a URL', function () {
      const webhook = new IncomingWebhook(url);
      assert.instanceOf(webhook, IncomingWebhook);
    });
  });

  describe('send()', function () {
    beforeEach(function () {
      this.webhook = new IncomingWebhook(url);
    });

    describe('when making a successful call', function () {
      beforeEach(function () {
        this.scope = nock('https://hooks.slack.com')
          .post(/services/)
          .reply(200, 'ok');
      });

      it('should return results in a Promise', function () {
        const result = this.webhook.send('Hello');
        assert(isPromise(result));
        return result.then((result) => {
          assert.strictEqual(result.text, 'ok');
          this.scope.done();
        });
      });

      it('should deliver results in a callback', function (done) {
        this.webhook.send('Hello', (error, result) => {
          assert.notOk(error);
          assert.strictEqual(result.text, 'ok');
          this.scope.done();
          done();
        });
      });
    });

    describe('when the call fails', function () {
      beforeEach(function () {
        this.scope = nock('https://hooks.slack.com')
          .post(/services/)
          .reply(500);
      });

      it('should return a Promise which rejects on error', function (done) {
        const result = this.webhook.send('Hello');
        assert(isPromise(result));
        result.catch((error) => {
          assert.ok(error);
          assert.instanceOf(error, Error);
          done();
        });
      });

      it('should deliver the error in a callback', function (done) {
        this.webhook.send('Hello', (error) => {
          assert.ok(error);
          assert.instanceOf(error, Error);
          done();
        });
      });

      it('should fail with IncomingWebhookRequestError when the API request fails', function (done) {
        // One known request error is when the node encounters an ECONNREFUSED. In order to simulate this, rather than
        // using nock, we send the request to a host:port that is not listening.
        const webhook = new IncomingWebhook('https://localhost:8999/api/');
        webhook.send('Hello', (error) => {
          assert.instanceOf(error, Error);
          assert.equal(error.code, ErrorCode.IncomingWebhookRequestError);
          assert.instanceOf(error.original, Error);
          done();
        });
      });
    });

    describe('lifecycle', function () {
      beforeEach(function () {
        this.scope = nock('https://hooks.slack.com')
          .post(/services/)
          .reply(500);
      });

      it('should not overwrite the default parameters after a call', function (done) {
        const defaultParams  = { channel: 'default' };
        const expectedParams = Object.assign({}, defaultParams);
        const webhook        = new IncomingWebhook(url, defaultParams);

        webhook.send({ channel: 'different' }, () => {
          assert.deepEqual(webhook.defaults, expectedParams);
          done();
        });
      });
    });
  });

  describe('has an option to set a custom HTTP agent', function () {
    it('should send a request using the custom agent', function (done) {
      const agent = new Agent({keepAlive: true});
      const spy = sinon.spy(agent, 'addRequest');
      const webhook = new IncomingWebhook(url, {agent});
      webhook.send('Hello', () => {
        // assert(spy.called);
        agent.addRequest.restore();
        agent.destroy();
        done();
      });
    });

    it('should use accept a boolean (false) agent', function (done) {
      const webhook = new IncomingWebhook(url, {agent: false});
      webhook.send('Hello', () => {
        done();
      });
    });
  });

});
<|endoftext|>