diff --git a/api/spec/packages/aip-client-javascript/README.md b/api/spec/packages/aip-client-javascript/README.md index d9dcf45b12..f6b1698a4d 100644 --- a/api/spec/packages/aip-client-javascript/README.md +++ b/api/spec/packages/aip-client-javascript/README.md @@ -171,7 +171,8 @@ The full call path, HTTP route, and a short description are listed below. | `client.subscriptions.cancel` | `POST /openmeter/subscriptions/{subscriptionId}/cancel` | Cancels the subscription. Will result in a scheduling conflict if there are other subscriptions scheduled to start after the cancelation time. | | `client.subscriptions.unscheduleCancelation` | `POST /openmeter/subscriptions/{subscriptionId}/unschedule-cancelation` | Unschedules the subscription cancelation. | | `client.subscriptions.change` | `POST /openmeter/subscriptions/{subscriptionId}/change` | Closes a running subscription and starts a new one according to the specification. Can be used for upgrades, downgrades, and plan changes. | -| `client.subscriptions.listAddons` | `GET /openmeter/subscriptions/{subscriptionId}/addons` | List the addons of a subscription. | +| `client.subscriptions.createAddon` | `POST /openmeter/subscriptions/{subscriptionId}/addons` | Add add-on to a subscription. | +| `client.subscriptions.listAddons` | `GET /openmeter/subscriptions/{subscriptionId}/addons` | List the add-ons of a subscription. | | `client.subscriptions.getAddon` | `GET /openmeter/subscriptions/{subscriptionId}/addons/{subscriptionAddonId}` | Get an add-on association for a subscription. | ### Apps diff --git a/api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts b/api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts index b8cd4ef40e..5dc2bccf02 100644 --- a/api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts +++ b/api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts @@ -15,6 +15,8 @@ import type { UnscheduleCancelationResponse, ChangeSubscriptionRequest, ChangeSubscriptionResponse, + CreateSubscriptionAddonRequest, + CreateSubscriptionAddonResponse, ListSubscriptionAddonsRequest, ListSubscriptionAddonsResponse, GetSubscriptionAddonRequest, @@ -107,6 +109,21 @@ export function changeSubscription( ) } +export function createSubscriptionAddon( + client: Client, + req: CreateSubscriptionAddonRequest, + options?: RequestOptions, +): Promise> { + const path = encodePath('openmeter/subscriptions/{subscriptionId}/addons', { + subscriptionId: req.subscriptionId, + }) + return request(() => + http(client) + .post(path, { ...options, json: req.body }) + .json(), + ) +} + export function listSubscriptionAddons( client: Client, req: ListSubscriptionAddonsRequest, diff --git a/api/spec/packages/aip-client-javascript/src/index.ts b/api/spec/packages/aip-client-javascript/src/index.ts index ea535f596f..d53c519d79 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -66,6 +66,8 @@ export type { TaxConfigExternalInvoicing, ChargeFlatFeeDiscounts, PriceFree, + RateCardStaticEntitlement, + RateCardBooleanEntitlement, WorkflowCollectionAlignmentSubscription, WorkflowPaymentChargeAutomaticallySettings, WorkflowPaymentSendInvoiceSettings, @@ -75,8 +77,6 @@ export type { InvoiceLineExternalReferences, LlmCostProvider, LlmCostModel, - RateCardStaticEntitlement, - RateCardBooleanEntitlement, ProductCatalogValidationError, GovernanceQueryRequestCustomers, GovernanceQueryRequestFeatures, @@ -97,11 +97,11 @@ export type { PriceUnit, RateCardDiscounts, Totals, + SpendCommitments, InvoiceLineCreditsApplied, FeatureManualUnitCost, FeatureLlmUnitCostPricing, LlmCostModelPricing, - SpendCommitments, QueryFilterNumeric, CursorPaginationQuery, ListMetersParamsFilter, @@ -115,14 +115,15 @@ export type { BillingCustomerReference, SubscriptionReference, AddonReference, + FeatureReference, AppReference, ChargeReference, CurrencyFiat, - FeatureReference, Event, MeterQueryRow, AppStripeCreateCustomerPortalSessionResult, ClosedPeriod, + SubscriptionAddonTimelineSegment, CostBasis, CreateCostBasisRequest, FeatureCostQueryRow, @@ -165,8 +166,8 @@ export type { CustomerStripeCreateCustomerPortalSessionRequest, EntitlementAccessResult, CreateCreditGrantPurchase, - RecurringPeriod, RateCardMeteredEntitlement, + RecurringPeriod, CreditGrantPurchase, UpdateCreditGrantExternalSettlementRequest, ListCreditGrantsParamsFilter, @@ -176,6 +177,7 @@ export type { SubscriptionCreate, RateCardProrationConfiguration, Subscription, + UnitConfig, AppCatalogItem, TaxCodeAppMapping, PartyTaxIdentity, @@ -188,7 +190,6 @@ export type { ListCurrenciesParamsFilter, CurrencyCustom, CreateCurrencyCustomRequest, - UnitConfig, GovernanceQueryRequest, GovernanceFeatureAccessReason, GovernanceQueryError, @@ -214,7 +215,6 @@ export type { RateCardTaxConfig, OrganizationDefaultTaxCodes, UpdateOrganizationDefaultTaxCodesRequest, - SubscriptionAddon, PlanAddon, CreatePlanAddonRequest, ProfileAppReferences, @@ -241,6 +241,8 @@ export type { SubscriptionChangeResponse, SubscriptionCancel, SubscriptionChange, + CreateSubscriptionAddonRequest, + InvoiceUsageQuantityDetail, AppStripe, AppSandbox, AppExternalInvoicing, @@ -250,7 +252,6 @@ export type { InvoiceWorkflow, InvoiceStatusDetails, InvoiceLineDiscounts, - InvoiceUsageQuantityDetail, GovernanceFeatureAccess, CustomerData, UpsertCustomerBillingDataRequest, @@ -263,7 +264,6 @@ export type { CreditGrant, CreateChargeFlatFeeRequest, WorkflowTaxSettings, - SubscriptionAddonPagePaginatedResponse, PlanAddonPagePaginatedResponse, IngestedEventPaginatedResponse, InvalidParameters, @@ -291,26 +291,29 @@ export type { ChargeFlatFee, ChargeUsageBased, CreateChargeUsageBasedRequest, - InvoiceLineRateCard, RateCard, + InvoiceLineRateCard, FeaturePagePaginatedResponse, Workflow, - InvoiceStandardLine, + SubscriptionAddonRateCard, PlanPhase, Addon, CreateAddonRequest, UpsertAddonRequest, + InvoiceStandardLine, Profile, CreateBillingProfileRequest, UpsertBillingProfileRequest, ChargePagePaginatedResponse, + SubscriptionAddon, Plan, CreatePlanRequest, UpsertPlanRequest, AddonPagePaginatedResponse, ProfilePagePaginatedResponse, - InvoiceStandard, + SubscriptionAddonPagePaginatedResponse, PlanPagePaginatedResponse, + InvoiceStandard, SortQueryInput, BaseErrorInput, WorkflowPaymentSendInvoiceSettingsInput, @@ -333,13 +336,13 @@ export type { CreateCreditGrantPurchaseInput, RateCardMeteredEntitlementInput, CreditGrantPurchaseInput, - WorkflowInvoicingSettingsInput, UnitConfigInput, + WorkflowInvoicingSettingsInput, GovernanceQueryRequestInput, IngestedEventInput, SubscriptionCancelInput, - InvoiceWorkflowInput, InvoiceUsageQuantityDetailInput, + InvoiceWorkflowInput, CreateCreditGrantRequestInput, CreditGrantInput, WorkflowTaxSettingsInput, @@ -354,19 +357,22 @@ export type { WorkflowCollectionSettingsInput, RateCardInput, WorkflowInput, - InvoiceStandardLineInput, + SubscriptionAddonRateCardInput, PlanPhaseInput, AddonInput, CreateAddonRequestInput, UpsertAddonRequestInput, + InvoiceStandardLineInput, ProfileInput, CreateBillingProfileRequestInput, UpsertBillingProfileRequestInput, + SubscriptionAddonInput, PlanInput, CreatePlanRequestInput, UpsertPlanRequestInput, AddonPagePaginatedResponseInput, ProfilePagePaginatedResponseInput, - InvoiceStandardInput, + SubscriptionAddonPagePaginatedResponseInput, PlanPagePaginatedResponseInput, + InvoiceStandardInput, } from './models/types.js' diff --git a/api/spec/packages/aip-client-javascript/src/models/operations/subscriptions.ts b/api/spec/packages/aip-client-javascript/src/models/operations/subscriptions.ts index 6e0b9d2ce6..d052cff7a0 100644 --- a/api/spec/packages/aip-client-javascript/src/models/operations/subscriptions.ts +++ b/api/spec/packages/aip-client-javascript/src/models/operations/subscriptions.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import * as schemas from '../schemas.js' import type { + CreateSubscriptionAddonRequest as CreateSubscriptionAddonRequestBody, ListSubscriptionsParamsFilter, SortQueryInput, Subscription, @@ -59,6 +60,12 @@ export type ChangeSubscriptionRequest = { } export type ChangeSubscriptionResponse = SubscriptionChangeResponse +export type CreateSubscriptionAddonRequest = { + subscriptionId: string + body: CreateSubscriptionAddonRequestBody +} +export type CreateSubscriptionAddonResponse = SubscriptionAddon + export interface ListSubscriptionAddonsQuery { /** Determines which page of the collection to retrieve. */ page?: { size?: number; number?: number } diff --git a/api/spec/packages/aip-client-javascript/src/models/schemas.ts b/api/spec/packages/aip-client-javascript/src/models/schemas.ts index 90a3dd4a99..b118c985cb 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -668,6 +668,40 @@ export const subscriptionEditTimingEnum = z 'Subscription edit timing. When immediate, the requested changes take effect immediately. When next_billing_cycle, the requested changes take effect at the next billing cycle.', ) +export const unitConfigOperation = z + .enum(['divide', 'multiply']) + + .describe( + 'The arithmetic operation used to convert raw metered units into billing units. - `divide`: Divide the metered quantity by the conversion factor (e.g., bytes ÷ 1e9 = GB). - `multiply`: Multiply the metered quantity by the conversion factor (e.g., cost × 1.2 = cost + 20% margin).', + ) + +export const unitConfigRoundingMode = z + .enum(['ceiling', 'floor', 'half_up', 'none']) + + .describe( + 'The rounding mode applied to the converted quantity for invoicing. Rounding is applied only to the invoiced quantity. Entitlement balance checks use the precise decimal value after conversion. - `ceiling`: Round up to the next integer (typical for package-style billing). - `floor`: Round down to the previous integer. - `half_up`: Round to the nearest integer, with 0.5 rounding up. - `none`: No rounding; the converted value is used as-is.', + ) + +export const rateCardStaticEntitlement = z + .object({ + type: z.literal('static').describe('The type of the entitlement template.'), + config: z + .unknown() + + .describe( + 'The entitlement config as a JSON object. Returned when checking entitlement access; useful for configuring fine-grained access settings implemented in your own system.', + ), + }) + .describe('The entitlement template of a static entitlement.') + +export const rateCardBooleanEntitlement = z + .object({ + type: z + .literal('boolean') + .describe('The type of the entitlement template.'), + }) + .describe('The entitlement template of a boolean entitlement.') + export const appType = z .enum(['sandbox', 'stripe', 'external_invoicing']) .describe('The type of the app.') @@ -896,40 +930,6 @@ export const planStatus = z 'The status of a plan. - `draft`: The plan has not yet been published and can be edited. - `active`: The plan is published and can be used in subscriptions. - `archived`: The plan is no longer available for use. - `scheduled`: The plan is scheduled to be published at a future date.', ) -export const unitConfigOperation = z - .enum(['divide', 'multiply']) - - .describe( - 'The arithmetic operation used to convert raw metered units into billing units. - `divide`: Divide the metered quantity by the conversion factor (e.g., bytes ÷ 1e9 = GB). - `multiply`: Multiply the metered quantity by the conversion factor (e.g., cost × 1.2 = cost + 20% margin).', - ) - -export const unitConfigRoundingMode = z - .enum(['ceiling', 'floor', 'half_up', 'none']) - - .describe( - 'The rounding mode applied to the converted quantity for invoicing. Rounding is applied only to the invoiced quantity. Entitlement balance checks use the precise decimal value after conversion. - `ceiling`: Round up to the next integer (typical for package-style billing). - `floor`: Round down to the previous integer. - `half_up`: Round to the nearest integer, with 0.5 rounding up. - `none`: No rounding; the converted value is used as-is.', - ) - -export const rateCardStaticEntitlement = z - .object({ - type: z.literal('static').describe('The type of the entitlement template.'), - config: z - .unknown() - - .describe( - 'The entitlement config as a JSON object. Returned when checking entitlement access; useful for configuring fine-grained access settings implemented in your own system.', - ), - }) - .describe('The entitlement template of a static entitlement.') - -export const rateCardBooleanEntitlement = z - .object({ - type: z - .literal('boolean') - .describe('The type of the entitlement template.'), - }) - .describe('The entitlement template of a boolean entitlement.') - export const productCatalogValidationError = z .object({ code: z.string().describe('Machine-readable error code.'), @@ -1376,6 +1376,16 @@ export const totals = z 'Totals contains the summaries of all calculations for a billing resource.', ) +export const spendCommitments = z + .object({ + minimum_amount: numeric.optional(), + maximum_amount: numeric.optional(), + }) + + .describe( + 'Spend commitments for a rate card. The customer is committed to spend at least the minimum amount and at most the maximum amount.', + ) + export const invoiceLineCreditsApplied = z .object({ amount: numeric, @@ -1418,16 +1428,6 @@ export const llmCostModelPricing = z }) .describe('Token pricing for an LLM model, denominated per token.') -export const spendCommitments = z - .object({ - minimum_amount: numeric.optional(), - maximum_amount: numeric.optional(), - }) - - .describe( - 'Spend commitments for a rate card. The customer is committed to spend at least the minimum amount and at most the maximum amount.', - ) - export const queryFilterNumeric = z .object({ gt: numeric.optional(), @@ -1573,6 +1573,12 @@ export const addonReference = z }) .describe('Addon reference.') +export const featureReference = z + .object({ + id: ulid, + }) + .describe('Feature reference.') + export const appReference = z .object({ id: ulid, @@ -1615,12 +1621,6 @@ export const currencyFiat = z }) .describe('Currency describes a currency supported by the billing system.') -export const featureReference = z - .object({ - id: ulid, - }) - .describe('Feature reference.') - export const dateTimeFieldFilter = z .union([ dateTime, @@ -1764,6 +1764,18 @@ export const closedPeriod = z 'A period with defined start and end dates. The period is always inclusive at the start and exclusive at the end.', ) +export const subscriptionAddonTimelineSegment = z + .object({ + active_from: dateTime, + active_to: dateTime.optional(), + quantity: z + .number() + .int() + .nonnegative() + .describe('The quantity of the add-on for the given period.'), + }) + .describe('A subscription add-on event.') + export const costBasis = z .object({ id: ulid, @@ -2442,13 +2454,6 @@ export const createCreditGrantPurchase = z }) .describe('Purchase and payment terms of the grant.') -export const recurringPeriod = z - .object({ - anchor: dateTime, - interval: iso8601Duration, - }) - .describe('Recurring period with an anchor and an interval.') - export const rateCardMeteredEntitlement = z .object({ type: z @@ -2474,6 +2479,13 @@ export const rateCardMeteredEntitlement = z }) .describe('The entitlement template of a metered entitlement.') +export const recurringPeriod = z + .object({ + anchor: dateTime, + interval: iso8601Duration, + }) + .describe('Recurring period with an anchor and an interval.') + export const creditGrantPurchase = z .object({ currency: currencyCode, @@ -2580,6 +2592,33 @@ export const subscriptionEditTiming = z 'Subscription edit timing defined when the changes should take effect. If the provided configuration is not supported by the subscription, an error will be returned.', ) +export const unitConfig = z + .object({ + operation: unitConfigOperation, + conversion_factor: numeric, + rounding: unitConfigRoundingMode.optional().default('none'), + precision: z + .number() + .int() + .optional() + .default(0) + + .describe( + 'The number of decimal places to retain after rounding. Only meaningful when rounding is not "none". Defaults to 0 (round to whole numbers).', + ), + display_unit: z + .string() + .optional() + + .describe( + 'A human-readable label for the converted unit shown on invoices and in the customer portal (e.g., "GB", "hours", "M tokens"). Optional. When omitted, no unit label is rendered.', + ), + }) + + .describe( + 'Unit conversion configuration. Transforms raw metered quantities into billing-ready units before pricing and entitlement evaluation. Applied at the rate card level so the same feature can be billed in different units across plans. Examples: - Meter bytes, bill GB: operation=divide, conversionFactor=1e9, rounding=ceiling, displayUnit="GB" - Meter seconds, bill hours: operation=divide, conversionFactor=3600, rounding=ceiling, displayUnit="hours" - Cost + 20% margin: operation=multiply, conversionFactor=1.2 - Bill per million tokens: operation=divide, conversionFactor=1e6, rounding=ceiling, displayUnit="M" v1 equivalents: - DynamicPrice(multiplier): operation=multiply, conversionFactor=multiplier + UnitPrice(amount=1) - PackagePrice(amount, quantityPerPkg): operation=divide, conversionFactor=quantityPerPkg, rounding=ceiling + UnitPrice(amount)', + ) + export const appCatalogItem = z .object({ type: appType, @@ -2783,33 +2822,6 @@ export const createCurrencyCustomRequest = z }) .describe('CurrencyCustom create request.') -export const unitConfig = z - .object({ - operation: unitConfigOperation, - conversion_factor: numeric, - rounding: unitConfigRoundingMode.optional().default('none'), - precision: z - .number() - .int() - .optional() - .default(0) - - .describe( - 'The number of decimal places to retain after rounding. Only meaningful when rounding is not "none". Defaults to 0 (round to whole numbers).', - ), - display_unit: z - .string() - .optional() - - .describe( - 'A human-readable label for the converted unit shown on invoices and in the customer portal (e.g., "GB", "hours", "M tokens"). Optional. When omitted, no unit label is rendered.', - ), - }) - - .describe( - 'Unit conversion configuration. Transforms raw metered quantities into billing-ready units before pricing and entitlement evaluation. Applied at the rate card level so the same feature can be billed in different units across plans. Examples: - Meter bytes, bill GB: operation=divide, conversionFactor=1e9, rounding=ceiling, displayUnit="GB" - Meter seconds, bill hours: operation=divide, conversionFactor=3600, rounding=ceiling, displayUnit="hours" - Cost + 20% margin: operation=multiply, conversionFactor=1.2 - Bill per million tokens: operation=divide, conversionFactor=1e6, rounding=ceiling, displayUnit="M" v1 equivalents: - DynamicPrice(multiplier): operation=multiply, conversionFactor=multiplier + UnitPrice(amount=1) - PackagePrice(amount, quantityPerPkg): operation=divide, conversionFactor=quantityPerPkg, rounding=ceiling + UnitPrice(amount)', - ) - export const governanceQueryRequest = z .object({ include_credits: z @@ -3163,41 +3175,6 @@ export const updateOrganizationDefaultTaxCodesRequest = z }) .describe('OrganizationDefaultTaxCodes update request.') -export const subscriptionAddon = z - .object({ - id: ulid, - name: z - .string() - .min(1) - .max(256) - .describe('Display name of the resource. Between 1 and 256 characters.'), - description: z - .string() - .max(1024) - .optional() - - .describe( - 'Optional description of the resource. Maximum 1024 characters.', - ), - labels: labels.optional(), - created_at: dateTime, - updated_at: dateTime, - deleted_at: dateTime.optional(), - addon: addonReference, - quantity: z - .number() - .int() - .gte(1) - - .describe( - 'The quantity of the add-on. Always 1 for single instance add-ons.', - ), - quantity_at: dateTime, - active_from: dateTime, - active_to: dateTime.optional(), - }) - .describe('Addon purchased with a subscription.') - export const planAddon = z .object({ id: ulid, @@ -3533,16 +3510,6 @@ export const listCustomerEntitlementAccessResponseData = z }) .describe('List customer entitlement access response data.') -export const workflowCollectionAlignmentAnchored = z - .object({ - type: z.literal('anchored').describe('The type of alignment.'), - recurring_period: recurringPeriod, - }) - - .describe( - 'BillingWorkflowCollectionAlignmentAnchored specifies the alignment for collecting the pending line items into an invoice.', - ) - export const rateCardEntitlement = z .discriminatedUnion('type', [ rateCardMeteredEntitlement, @@ -3554,6 +3521,16 @@ export const rateCardEntitlement = z 'Entitlement template configured on a rate card. The feature is taken from the rate card itself, so it is omitted here.', ) +export const workflowCollectionAlignmentAnchored = z + .object({ + type: z.literal('anchored').describe('The type of alignment.'), + recurring_period: recurringPeriod, + }) + + .describe( + 'BillingWorkflowCollectionAlignmentAnchored specifies the alignment for collecting the pending line items into an invoice.', + ) + export const subscriptionPagePaginatedResponse = z .object({ data: z.array(subscription), @@ -3603,6 +3580,39 @@ export const subscriptionChange = z }) .describe('Request for changing a subscription.') +export const createSubscriptionAddonRequest = z + .object({ + labels: labels.optional(), + addon: addonReference, + quantity: z + .number() + .int() + .gte(1) + + .describe( + 'The quantity of the add-on. Always 1 for single instance add-ons.', + ), + timing: subscriptionEditTiming, + }) + .describe('SubscriptionAddon create request.') + +export const invoiceUsageQuantityDetail = z + .object({ + raw_quantity: numeric, + converted_quantity: numeric, + invoiced_quantity: numeric, + display_unit: z + .string() + .optional() + + .describe('The display unit label (e.g., "GB", "hours", "M tokens").'), + applied_unit_config: unitConfig, + }) + + .describe( + 'Usage quantity details on an invoice line item when UnitConfig is in effect. Provides the full audit trail from raw meter output to the invoiced amount.', + ) + export const appStripe = z .object({ id: ulid, @@ -3839,23 +3849,6 @@ export const currency = z .discriminatedUnion('type', [currencyFiat, currencyCustom]) .describe('Fiat or custom currency.') -export const invoiceUsageQuantityDetail = z - .object({ - raw_quantity: numeric, - converted_quantity: numeric, - invoiced_quantity: numeric, - display_unit: z - .string() - .optional() - - .describe('The display unit label (e.g., "GB", "hours", "M tokens").'), - applied_unit_config: unitConfig, - }) - - .describe( - 'Usage quantity details on an invoice line item when UnitConfig is in effect. Provides the full audit trail from raw meter output to the invoiced amount.', - ) - export const governanceFeatureAccess = z .object({ has_access: z @@ -4088,13 +4081,6 @@ export const workflowTaxSettings = z }) .describe('Tax settings for a billing workflow.') -export const subscriptionAddonPagePaginatedResponse = z - .object({ - data: z.array(subscriptionAddon), - meta: paginatedMeta, - }) - .describe('Page paginated response.') - export const planAddonPagePaginatedResponse = z .object({ data: z.array(planAddon), @@ -4659,15 +4645,6 @@ export const createChargeUsageBasedRequest = z }) .describe('Usage-based charge create request.') -export const invoiceLineRateCard = z - .object({ - price: price, - tax_config: rateCardTaxConfig.optional(), - feature_key: resourceKey.optional(), - discounts: rateCardDiscounts.optional(), - }) - .describe('Rate card configuration snapshot for a usage-based invoice line.') - export const rateCard = z .object({ name: z @@ -4700,6 +4677,15 @@ export const rateCard = z 'A rate card defines the pricing and entitlement of a feature or service.', ) +export const invoiceLineRateCard = z + .object({ + price: price, + tax_config: rateCardTaxConfig.optional(), + feature_key: resourceKey.optional(), + discounts: rateCardDiscounts.optional(), + }) + .describe('Rate card configuration snapshot for a usage-based invoice line.') + export const featurePagePaginatedResponse = z .object({ data: z.array(feature), @@ -4727,52 +4713,17 @@ export const createChargeRequest = z ]) .describe('Customer charge.') -export const invoiceStandardLine = z +export const subscriptionAddonRateCard = z .object({ - id: ulid, - name: z - .string() - .min(1) - .max(256) - .describe('Display name of the resource. Between 1 and 256 characters.'), - description: z - .string() - .max(1024) - .optional() - - .describe( - 'Optional description of the resource. Maximum 1024 characters.', - ), - labels: labels.optional(), - created_at: dateTime, - updated_at: dateTime, - deleted_at: dateTime.optional(), - type: z - .literal('standard_line') - .describe('The type of charge this line item represents.'), - lifecycle_controller: lifecycleController, - service_period: closedPeriod, - totals: totals, - discounts: invoiceLineDiscounts.optional(), - credits_applied: z - .array(invoiceLineCreditsApplied) - .optional() - .describe('Credit applied to this line item.'), - external_references: invoiceLineExternalReferences.optional(), - subscription: subscriptionReference.optional(), - rate_card: invoiceLineRateCard, - detailed_lines: z - .array(invoiceDetailedLine) + rate_card: rateCard, + affected_subscription_item_ids: z + .array(ulid) .describe( - 'Detailed sub-lines that this line has been broken down into. Present when line has individual details.', + 'The IDs of the subscription items that this rate card belongs to.', ), - charge: chargeReference.optional(), }) - - .describe( - 'A top-level line item on an invoice. Each line represents a single charge, typically associated with a rate card from a subscription. Detailed (child) lines are nested under `detailed_lines` when present.', - ) + .describe('A rate card for a subscription add-on.') export const planPhase = z .object({ @@ -4889,6 +4840,53 @@ export const upsertAddonRequest = z }) .describe('Addon upsert request.') +export const invoiceStandardLine = z + .object({ + id: ulid, + name: z + .string() + .min(1) + .max(256) + .describe('Display name of the resource. Between 1 and 256 characters.'), + description: z + .string() + .max(1024) + .optional() + + .describe( + 'Optional description of the resource. Maximum 1024 characters.', + ), + labels: labels.optional(), + created_at: dateTime, + updated_at: dateTime, + deleted_at: dateTime.optional(), + type: z + .literal('standard_line') + .describe('The type of charge this line item represents.'), + lifecycle_controller: lifecycleController, + service_period: closedPeriod, + totals: totals, + discounts: invoiceLineDiscounts.optional(), + credits_applied: z + .array(invoiceLineCreditsApplied) + .optional() + .describe('Credit applied to this line item.'), + external_references: invoiceLineExternalReferences.optional(), + subscription: subscriptionReference.optional(), + rate_card: invoiceLineRateCard, + detailed_lines: z + .array(invoiceDetailedLine) + + .describe( + 'Detailed sub-lines that this line has been broken down into. Present when line has individual details.', + ), + charge: chargeReference.optional(), + }) + + .describe( + 'A top-level line item on an invoice. Each line represents a single charge, typically associated with a rate card from a subscription. Detailed (child) lines are nested under `detailed_lines` when present.', + ) + export const profile = z .object({ id: ulid, @@ -4971,12 +4969,50 @@ export const chargePagePaginatedResponse = z }) .describe('Page paginated response.') -export const invoiceLine = z - .discriminatedUnion('type', [invoiceStandardLine]) +export const subscriptionAddon = z + .object({ + id: ulid, + labels: labels.optional(), + created_at: dateTime, + updated_at: dateTime, + deleted_at: dateTime.optional(), + name: z + .string() + .min(1) + .max(256) + .describe('Display name of the resource. Between 1 and 256 characters.'), + description: z + .string() + .max(1024) + .optional() - .describe( - 'A top-level line item on an invoice. Each line represents a single charge, typically associated with a rate card from a subscription. Detailed (child) lines are nested under `detailed_lines` when present.', - ) + .describe( + 'Optional description of the resource. Maximum 1024 characters.', + ), + addon: addonReference, + quantity: z + .number() + .int() + .gte(1) + + .describe( + 'The quantity of the add-on. Always 1 for single instance add-ons.', + ), + quantity_at: dateTime, + active_from: dateTime, + active_to: dateTime.optional(), + timing: subscriptionEditTiming, + timeline: z + .array(subscriptionAddonTimelineSegment) + + .describe( + 'The timeline of the add-on. The returned periods are sorted and continuous.', + ), + rate_cards: z + .array(subscriptionAddonRateCard) + .describe('The rate cards of the add-on.'), + }) + .describe('Addon purchased with a subscription.') export const plan = z .object({ @@ -5108,6 +5144,13 @@ export const addonPagePaginatedResponse = z }) .describe('Page paginated response.') +export const invoiceLine = z + .discriminatedUnion('type', [invoiceStandardLine]) + + .describe( + 'A top-level line item on an invoice. Each line represents a single charge, typically associated with a rate card from a subscription. Detailed (child) lines are nested under `detailed_lines` when present.', + ) + export const profilePagePaginatedResponse = z .object({ data: z.array(profile), @@ -5115,6 +5158,20 @@ export const profilePagePaginatedResponse = z }) .describe('Page paginated response.') +export const subscriptionAddonPagePaginatedResponse = z + .object({ + data: z.array(subscriptionAddon), + meta: paginatedMeta, + }) + .describe('Page paginated response.') + +export const planPagePaginatedResponse = z + .object({ + data: z.array(plan), + meta: paginatedMeta, + }) + .describe('Page paginated response.') + export const invoiceStandard = z .object({ id: ulid, @@ -5166,13 +5223,6 @@ export const invoiceStandard = z }) .describe('A standard invoice for charges owed by the customer.') -export const planPagePaginatedResponse = z - .object({ - data: z.array(plan), - meta: paginatedMeta, - }) - .describe('Page paginated response.') - export const invoice = z .discriminatedUnion('type', [invoiceStandard]) @@ -5517,6 +5567,14 @@ export const changeSubscriptionBody = subscriptionChange export const changeSubscriptionResponse = subscriptionChangeResponse +export const createSubscriptionAddonPathParams = z.object({ + subscriptionId: ulid, +}) + +export const createSubscriptionAddonBody = createSubscriptionAddonRequest + +export const createSubscriptionAddonResponse = subscriptionAddon + export const listSubscriptionAddonsPathParams = z.object({ subscriptionId: ulid, }) diff --git a/api/spec/packages/aip-client-javascript/src/models/types.ts b/api/spec/packages/aip-client-javascript/src/models/types.ts index 0a7172c7d3..4ac4553123 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -179,6 +179,24 @@ export interface PriceFree { type: 'free' } +/** The entitlement template of a static entitlement. */ +export interface RateCardStaticEntitlement { + /** The type of the entitlement template. */ + type: 'static' + /** + * The entitlement config as a JSON object. Returned when checking entitlement + * access; useful for configuring fine-grained access settings implemented in your + * own system. + */ + config: unknown +} + +/** The entitlement template of a boolean entitlement. */ +export interface RateCardBooleanEntitlement { + /** The type of the entitlement template. */ + type: 'boolean' +} + /** * BillingWorkflowCollectionAlignmentSubscription specifies the alignment for * collecting the pending line items into an invoice. @@ -261,24 +279,6 @@ export interface LlmCostModel { name: string } -/** The entitlement template of a static entitlement. */ -export interface RateCardStaticEntitlement { - /** The type of the entitlement template. */ - type: 'static' - /** - * The entitlement config as a JSON object. Returned when checking entitlement - * access; useful for configuring fine-grained access settings implemented in your - * own system. - */ - config: unknown -} - -/** The entitlement template of a boolean entitlement. */ -export interface RateCardBooleanEntitlement { - /** The type of the entitlement template. */ - type: 'boolean' -} - /** Validation errors providing detailed description of the issue. */ export interface ProductCatalogValidationError { /** Machine-readable error code. */ @@ -515,6 +515,17 @@ export interface Totals { total: string } +/** + * Spend commitments for a rate card. The customer is committed to spend at least + * the minimum amount and at most the maximum amount. + */ +export interface SpendCommitments { + /** The customer is committed to spend at least the amount. */ + minimum_amount?: string + /** The customer is limited to spend at most the amount. */ + maximum_amount?: string +} + /** A credit allocation applied to an invoice line item. */ export interface InvoiceLineCreditsApplied { /** The monetary amount credited. */ @@ -559,17 +570,6 @@ export interface LlmCostModelPricing { reasoning_per_token?: string } -/** - * Spend commitments for a rate card. The customer is committed to spend at least - * the minimum amount and at most the maximum amount. - */ -export interface SpendCommitments { - /** The customer is committed to spend at least the amount. */ - minimum_amount?: string - /** The customer is limited to spend at most the amount. */ - maximum_amount?: string -} - /** * A query filter for a numeric attribute. Operators are mutually exclusive, only * one operator is allowed at a time. @@ -783,6 +783,11 @@ export interface AddonReference { id: string } +/** Feature reference. */ +export interface FeatureReference { + id: string +} + /** App reference. */ export interface AppReference { /** The ID of the app. */ @@ -815,11 +820,6 @@ export interface CurrencyFiat { code: string } -/** Feature reference. */ -export interface FeatureReference { - id: string -} - /** Metering event following the CloudEvents specification. */ export interface Event { /** Identifies the event. */ @@ -945,6 +945,16 @@ export interface ClosedPeriod { to: string } +/** A subscription add-on event. */ +export interface SubscriptionAddonTimelineSegment { + /** An ISO-8601 timestamp representation of the cadence start of the resource. */ + active_from: string + /** An ISO-8601 timestamp representation of the cadence end of the resource. */ + active_to?: string + /** The quantity of the add-on for the given period. */ + quantity: number +} + /** Describes currency basis supported by billing system. */ export interface CostBasis { id: string @@ -1610,14 +1620,6 @@ export interface CreateCreditGrantPurchase { availability_policy: 'on_creation' } -/** Recurring period with an anchor and an interval. */ -export interface RecurringPeriod { - /** A date-time anchor to base the recurring period on. */ - anchor: string - /** The interval duration in ISO 8601 format. */ - interval: string -} - /** The entitlement template of a metered entitlement. */ export interface RateCardMeteredEntitlement { /** The type of the entitlement template. */ @@ -1641,6 +1643,14 @@ export interface RateCardMeteredEntitlement { usage_period?: string } +/** Recurring period with an anchor and an interval. */ +export interface RecurringPeriod { + /** A date-time anchor to base the recurring period on. */ + anchor: string + /** The interval duration in ISO 8601 format. */ + interval: string +} + /** Purchase and payment terms of the grant. */ export interface CreditGrantPurchase { /** Currency of the purchase amount. */ @@ -1835,6 +1845,65 @@ export interface Subscription { settlement_mode?: 'credit_then_invoice' | 'credit_only' } +/** + * Unit conversion configuration. + * + * Transforms raw metered quantities into billing-ready units before pricing and + * entitlement evaluation. Applied at the rate card level so the same feature can + * be billed in different units across plans. + * + * Examples: + * + * - Meter bytes, bill GB: operation=divide, conversionFactor=1e9, + * rounding=ceiling, displayUnit="GB" + * - Meter seconds, bill hours: operation=divide, conversionFactor=3600, + * rounding=ceiling, displayUnit="hours" + * - Cost + 20% margin: operation=multiply, conversionFactor=1.2 + * - Bill per million tokens: operation=divide, conversionFactor=1e6, + * rounding=ceiling, displayUnit="M" + * + * v1 equivalents: + * + * - DynamicPrice(multiplier): operation=multiply, conversionFactor=multiplier + + * UnitPrice(amount=1) + * - PackagePrice(amount, quantityPerPkg): operation=divide, + * conversionFactor=quantityPerPkg, rounding=ceiling + UnitPrice(amount) + */ +export interface UnitConfig { + /** The arithmetic operation to apply to the raw metered quantity. */ + operation: 'divide' | 'multiply' + /** + * The factor used in the conversion operation. + * + * - For `divide`: `converted = raw / conversionFactor`. + * - For `multiply`: `converted = raw × conversionFactor`. + * + * Must be a positive non-zero value. + */ + conversion_factor: string + /** + * The rounding mode applied to the converted quantity for invoicing. + * + * Defaults to none (no rounding). Entitlement checks always use the precise + * (unrounded) value. + */ + rounding: 'ceiling' | 'floor' | 'half_up' | 'none' + /** + * The number of decimal places to retain after rounding. + * + * Only meaningful when rounding is not "none". Defaults to 0 (round to whole + * numbers). + */ + precision: number + /** + * A human-readable label for the converted unit shown on invoices and in the + * customer portal (e.g., "GB", "hours", "M tokens"). + * + * Optional. When omitted, no unit label is rendered. + */ + display_unit?: string +} + /** * Available apps for billing integrations to connect with third-party services. * Apps can have various capabilities like syncing data from or to external @@ -2020,65 +2089,6 @@ export interface CreateCurrencyCustomRequest { code: string } -/** - * Unit conversion configuration. - * - * Transforms raw metered quantities into billing-ready units before pricing and - * entitlement evaluation. Applied at the rate card level so the same feature can - * be billed in different units across plans. - * - * Examples: - * - * - Meter bytes, bill GB: operation=divide, conversionFactor=1e9, - * rounding=ceiling, displayUnit="GB" - * - Meter seconds, bill hours: operation=divide, conversionFactor=3600, - * rounding=ceiling, displayUnit="hours" - * - Cost + 20% margin: operation=multiply, conversionFactor=1.2 - * - Bill per million tokens: operation=divide, conversionFactor=1e6, - * rounding=ceiling, displayUnit="M" - * - * v1 equivalents: - * - * - DynamicPrice(multiplier): operation=multiply, conversionFactor=multiplier + - * UnitPrice(amount=1) - * - PackagePrice(amount, quantityPerPkg): operation=divide, - * conversionFactor=quantityPerPkg, rounding=ceiling + UnitPrice(amount) - */ -export interface UnitConfig { - /** The arithmetic operation to apply to the raw metered quantity. */ - operation: 'divide' | 'multiply' - /** - * The factor used in the conversion operation. - * - * - For `divide`: `converted = raw / conversionFactor`. - * - For `multiply`: `converted = raw × conversionFactor`. - * - * Must be a positive non-zero value. - */ - conversion_factor: string - /** - * The rounding mode applied to the converted quantity for invoicing. - * - * Defaults to none (no rounding). Entitlement checks always use the precise - * (unrounded) value. - */ - rounding: 'ceiling' | 'floor' | 'half_up' | 'none' - /** - * The number of decimal places to retain after rounding. - * - * Only meaningful when rounding is not "none". Defaults to 0 (round to whole - * numbers). - */ - precision: number - /** - * A human-readable label for the converted unit shown on invoices and in the - * customer portal (e.g., "GB", "hours", "M tokens"). - * - * Optional. When omitted, no unit label is rendered. - */ - display_unit?: string -} - /** Query to evaluate feature access for a list of customers. */ export interface GovernanceQueryRequest { /** @@ -2636,43 +2646,6 @@ export interface UpdateOrganizationDefaultTaxCodesRequest { credit_grant_tax_code?: TaxCodeReference } -/** Addon purchased with a subscription. */ -export interface SubscriptionAddon { - id: string - /** - * Display name of the resource. - * - * Between 1 and 256 characters. - */ - name: string - /** - * Optional description of the resource. - * - * Maximum 1024 characters. - */ - description?: string - labels?: Labels - /** An ISO-8601 timestamp representation of entity creation date. */ - created_at: string - /** An ISO-8601 timestamp representation of entity last update date. */ - updated_at: string - /** An ISO-8601 timestamp representation of entity deletion date. */ - deleted_at?: string - /** The add-on associated with the subscription. */ - addon: AddonReference - /** The quantity of the add-on. Always 1 for single instance add-ons. */ - quantity: number - /** - * An ISO-8601 timestamp representation of which point in time the quantity was - * resolved to. - */ - quantity_at: string - /** An ISO-8601 timestamp representation of the cadence start of the resource. */ - active_from: string - /** An ISO-8601 timestamp representation of the cadence end of the resource. */ - active_to?: string -} - /** * PlanAddon represents an association between a plan and an add-on, controlling * which add-ons are available for purchase within a plan. @@ -3178,12 +3151,50 @@ export interface SubscriptionChange { timing: 'immediate' | 'next_billing_cycle' | string } -/** Stripe app. */ -export interface AppStripe { - id: string +/** SubscriptionAddon create request. */ +export interface CreateSubscriptionAddonRequest { + labels?: Labels + /** The add-on associated with the subscription. */ + addon: AddonReference + /** The quantity of the add-on. Always 1 for single instance add-ons. */ + quantity: number /** - * Display name of the resource. - * + * The timing of the operation. After the create or update, a new entry will be + * created in the timeline. + */ + timing: 'immediate' | 'next_billing_cycle' | string +} + +/** + * Usage quantity details on an invoice line item when UnitConfig is in effect. + * + * Provides the full audit trail from raw meter output to the invoiced amount. + */ +export interface InvoiceUsageQuantityDetail { + /** The raw quantity as reported by the meter (native units). */ + raw_quantity: string + /** + * The precise decimal value after applying the conversion operation and factor, + * before rounding. + */ + converted_quantity: string + /** The quantity after rounding, used for pricing. */ + invoiced_quantity: string + /** The display unit label (e.g., "GB", "hours", "M tokens"). */ + display_unit?: string + /** + * Snapshot of the UnitConfig that was in effect at billing time. Ensures + * historical invoices reflect the config that was actually applied. + */ + applied_unit_config: UnitConfig +} + +/** Stripe app. */ +export interface AppStripe { + id: string + /** + * Display name of the resource. + * * Between 1 and 256 characters. */ name: string @@ -3423,30 +3434,6 @@ export interface InvoiceLineDiscounts { usage?: InvoiceLineUsageDiscount[] } -/** - * Usage quantity details on an invoice line item when UnitConfig is in effect. - * - * Provides the full audit trail from raw meter output to the invoiced amount. - */ -export interface InvoiceUsageQuantityDetail { - /** The raw quantity as reported by the meter (native units). */ - raw_quantity: string - /** - * The precise decimal value after applying the conversion operation and factor, - * before rounding. - */ - converted_quantity: string - /** The quantity after rounding, used for pricing. */ - invoiced_quantity: string - /** The display unit label (e.g., "GB", "hours", "M tokens"). */ - display_unit?: string - /** - * Snapshot of the UnitConfig that was in effect at billing time. Ensures - * historical invoices reflect the config that was actually applied. - */ - applied_unit_config: UnitConfig -} - /** Access status for a single feature. */ export interface GovernanceFeatureAccess { /** @@ -3721,12 +3708,6 @@ export interface WorkflowTaxSettings { default_tax_config?: TaxConfig } -/** Page paginated response. */ -export interface SubscriptionAddonPagePaginatedResponse { - data: SubscriptionAddon[] - meta: PaginatedMeta -} - /** Page paginated response. */ export interface PlanAddonPagePaginatedResponse { data: PlanAddon[] @@ -4409,18 +4390,6 @@ export interface CreateChargeUsageBasedRequest { billing_period?: ClosedPeriod } -/** Rate card configuration snapshot for a usage-based invoice line. */ -export interface InvoiceLineRateCard { - /** The price definition used to calculate charges for this line. */ - price: PriceFree | PriceFlat | PriceUnit | PriceGraduated | PriceVolume - /** Tax configuration snapshot for this line. */ - tax_config?: RateCardTaxConfig - /** The feature key associated with this line's rate card. */ - feature_key?: string - /** Discount configuration from the rate card. */ - discounts?: RateCardDiscounts -} - /** A rate card defines the pricing and entitlement of a feature or service. */ export interface RateCard { /** @@ -4481,6 +4450,18 @@ export interface RateCard { | RateCardBooleanEntitlement } +/** Rate card configuration snapshot for a usage-based invoice line. */ +export interface InvoiceLineRateCard { + /** The price definition used to calculate charges for this line. */ + price: PriceFree | PriceFlat | PriceUnit | PriceGraduated | PriceVolume + /** Tax configuration snapshot for this line. */ + tax_config?: RateCardTaxConfig + /** The feature key associated with this line's rate card. */ + feature_key?: string + /** Discount configuration from the rate card. */ + discounts?: RateCardDiscounts +} + /** Page paginated response. */ export interface FeaturePagePaginatedResponse { data: Feature[] @@ -4501,69 +4482,12 @@ export interface Workflow { tax?: WorkflowTaxSettings } -/** - * A top-level line item on an invoice. - * - * Each line represents a single charge, typically associated with a rate card from - * a subscription. Detailed (child) lines are nested under `detailed_lines` when - * present. - */ -export interface InvoiceStandardLine { - id: string - /** - * Display name of the resource. - * - * Between 1 and 256 characters. - */ - name: string - /** - * Optional description of the resource. - * - * Maximum 1024 characters. - */ - description?: string - labels?: Labels - /** An ISO-8601 timestamp representation of entity creation date. */ - created_at: string - /** An ISO-8601 timestamp representation of entity last update date. */ - updated_at: string - /** An ISO-8601 timestamp representation of entity deletion date. */ - deleted_at?: string - /** The type of charge this line item represents. */ - type: 'standard_line' - /** - * Indicates whether this line item's lifecycle is controlled by OpenMeter or - * manually overridden by the API user. - */ - lifecycle_controller: 'system' | 'manual' - /** - * The service period covered by this invoice, spanning the earliest line start to - * the latest line end across all of its lines. - * - * For an invoice with no lines the period is empty, which means `from` will be - * equal to `to`. - */ - service_period: ClosedPeriod - /** Aggregated financial totals for the line item. */ - totals: Totals - /** Discounts applied to this line item. */ - discounts?: InvoiceLineDiscounts - /** Credit applied to this line item. */ - credits_applied?: InvoiceLineCreditsApplied[] - /** External identifiers for this line item assigned by third-party systems. */ - external_references?: InvoiceLineExternalReferences - /** Reference to the subscription item that generated this line. */ - subscription?: SubscriptionReference - /** The rate card configuration snapshot used to price this line item. */ - rate_card: InvoiceLineRateCard - /** - * Detailed sub-lines that this line has been broken down into. - * - * Present when line has individual details. - */ - detailed_lines: InvoiceDetailedLine[] - /** Reference to the charge associated with this line item. */ - charge?: ChargeReference +/** A rate card for a subscription add-on. */ +export interface SubscriptionAddonRateCard { + /** The rate card. */ + rate_card: RateCard + /** The IDs of the subscription items that this rate card belongs to. */ + affected_subscription_item_ids: string[] } /** @@ -4706,6 +4630,71 @@ export interface UpsertAddonRequest { rate_cards: RateCard[] } +/** + * A top-level line item on an invoice. + * + * Each line represents a single charge, typically associated with a rate card from + * a subscription. Detailed (child) lines are nested under `detailed_lines` when + * present. + */ +export interface InvoiceStandardLine { + id: string + /** + * Display name of the resource. + * + * Between 1 and 256 characters. + */ + name: string + /** + * Optional description of the resource. + * + * Maximum 1024 characters. + */ + description?: string + labels?: Labels + /** An ISO-8601 timestamp representation of entity creation date. */ + created_at: string + /** An ISO-8601 timestamp representation of entity last update date. */ + updated_at: string + /** An ISO-8601 timestamp representation of entity deletion date. */ + deleted_at?: string + /** The type of charge this line item represents. */ + type: 'standard_line' + /** + * Indicates whether this line item's lifecycle is controlled by OpenMeter or + * manually overridden by the API user. + */ + lifecycle_controller: 'system' | 'manual' + /** + * The service period covered by this invoice, spanning the earliest line start to + * the latest line end across all of its lines. + * + * For an invoice with no lines the period is empty, which means `from` will be + * equal to `to`. + */ + service_period: ClosedPeriod + /** Aggregated financial totals for the line item. */ + totals: Totals + /** Discounts applied to this line item. */ + discounts?: InvoiceLineDiscounts + /** Credit applied to this line item. */ + credits_applied?: InvoiceLineCreditsApplied[] + /** External identifiers for this line item assigned by third-party systems. */ + external_references?: InvoiceLineExternalReferences + /** Reference to the subscription item that generated this line. */ + subscription?: SubscriptionReference + /** The rate card configuration snapshot used to price this line item. */ + rate_card: InvoiceLineRateCard + /** + * Detailed sub-lines that this line has been broken down into. + * + * Present when line has individual details. + */ + detailed_lines: InvoiceDetailedLine[] + /** Reference to the charge associated with this line item. */ + charge?: ChargeReference +} + /** * Billing profiles contain the settings for billing and controls invoice * generation. @@ -4804,6 +4793,52 @@ export interface ChargePagePaginatedResponse { meta: PaginatedMeta } +/** Addon purchased with a subscription. */ +export interface SubscriptionAddon { + id: string + labels?: Labels + /** An ISO-8601 timestamp representation of entity creation date. */ + created_at: string + /** An ISO-8601 timestamp representation of entity last update date. */ + updated_at: string + /** An ISO-8601 timestamp representation of entity deletion date. */ + deleted_at?: string + /** + * Display name of the resource. + * + * Between 1 and 256 characters. + */ + name: string + /** + * Optional description of the resource. + * + * Maximum 1024 characters. + */ + description?: string + /** The add-on associated with the subscription. */ + addon: AddonReference + /** The quantity of the add-on. Always 1 for single instance add-ons. */ + quantity: number + /** + * An ISO-8601 timestamp representation of which point in time the quantity was + * resolved to. + */ + quantity_at: string + /** An ISO-8601 timestamp representation of the cadence start of the resource. */ + active_from: string + /** An ISO-8601 timestamp representation of the cadence end of the resource. */ + active_to?: string + /** + * The timing of the operation. After the create or update, a new entry will be + * created in the timeline. + */ + timing: 'immediate' | 'next_billing_cycle' | string + /** The timeline of the add-on. The returned periods are sorted and continuous. */ + timeline: SubscriptionAddonTimelineSegment[] + /** The rate cards of the add-on. */ + rate_cards: SubscriptionAddonRateCard[] +} + /** Plans provide a template for subscriptions. */ export interface Plan { id: string @@ -4955,6 +4990,18 @@ export interface ProfilePagePaginatedResponse { meta: PaginatedMeta } +/** Page paginated response. */ +export interface SubscriptionAddonPagePaginatedResponse { + data: SubscriptionAddon[] + meta: PaginatedMeta +} + +/** Page paginated response. */ +export interface PlanPagePaginatedResponse { + data: Plan[] + meta: PaginatedMeta +} + /** A standard invoice for charges owed by the customer. */ export interface InvoiceStandard { id: string @@ -5042,12 +5089,6 @@ export interface InvoiceStandard { lines?: InvoiceStandardLine[] } -/** Page paginated response. */ -export interface PlanPagePaginatedResponse { - data: Plan[] - meta: PaginatedMeta -} - export interface SortQueryInput { /** The attribute to sort by. */ by: string @@ -5247,17 +5288,6 @@ export interface CreditGrantPurchaseInput { settlement_status?: 'pending' | 'authorized' | 'settled' } -export interface WorkflowInvoicingSettingsInput { - /** Whether to automatically issue the invoice after the draftPeriod has passed. */ - auto_advance?: boolean - /** The period for the invoice to be kept in draft status for manual reviews. */ - draft_period?: string - /** Should progressive billing be allowed for this workflow? */ - progressive_billing?: boolean - /** Controls how subscription-ending shortened service periods are billed. */ - subscription_end_proration_mode?: 'bill_full_period' | 'bill_actual_period' -} - export interface UnitConfigInput { /** The arithmetic operation to apply to the raw metered quantity. */ operation: 'divide' | 'multiply' @@ -5293,6 +5323,17 @@ export interface UnitConfigInput { display_unit?: string } +export interface WorkflowInvoicingSettingsInput { + /** Whether to automatically issue the invoice after the draftPeriod has passed. */ + auto_advance?: boolean + /** The period for the invoice to be kept in draft status for manual reviews. */ + draft_period?: string + /** Should progressive billing be allowed for this workflow? */ + progressive_billing?: boolean + /** Controls how subscription-ending shortened service periods are billed. */ + subscription_end_proration_mode?: 'bill_full_period' | 'bill_actual_period' +} + export interface GovernanceQueryRequestInput { /** * Whether to include credit balance availability for each resolved customer. When @@ -5323,15 +5364,6 @@ export interface SubscriptionCancelInput { timing?: 'immediate' | 'next_billing_cycle' | string } -export interface InvoiceWorkflowInput { - /** Invoicing settings for this invoice. */ - invoicing?: InvoiceWorkflowInvoicingSettingsInput - /** Payment settings for this invoice. */ - payment?: - | WorkflowPaymentChargeAutomaticallySettings - | WorkflowPaymentSendInvoiceSettingsInput -} - export interface InvoiceUsageQuantityDetailInput { /** The raw quantity as reported by the meter (native units). */ raw_quantity: string @@ -5351,6 +5383,15 @@ export interface InvoiceUsageQuantityDetailInput { applied_unit_config: UnitConfigInput } +export interface InvoiceWorkflowInput { + /** Invoicing settings for this invoice. */ + invoicing?: InvoiceWorkflowInvoicingSettingsInput + /** Payment settings for this invoice. */ + payment?: + | WorkflowPaymentChargeAutomaticallySettings + | WorkflowPaymentSendInvoiceSettingsInput +} + export interface CreateCreditGrantRequestInput { /** * Display name of the resource. @@ -5761,62 +5802,11 @@ export interface WorkflowInput { tax?: WorkflowTaxSettingsInput } -export interface InvoiceStandardLineInput { - id: string - /** - * Display name of the resource. - * - * Between 1 and 256 characters. - */ - name: string - /** - * Optional description of the resource. - * - * Maximum 1024 characters. - */ - description?: string - labels?: Labels - /** An ISO-8601 timestamp representation of entity creation date. */ - created_at: string - /** An ISO-8601 timestamp representation of entity last update date. */ - updated_at: string - /** An ISO-8601 timestamp representation of entity deletion date. */ - deleted_at?: string - /** The type of charge this line item represents. */ - type: 'standard_line' - /** - * Indicates whether this line item's lifecycle is controlled by OpenMeter or - * manually overridden by the API user. - */ - lifecycle_controller: 'system' | 'manual' - /** - * The service period covered by this invoice, spanning the earliest line start to - * the latest line end across all of its lines. - * - * For an invoice with no lines the period is empty, which means `from` will be - * equal to `to`. - */ - service_period: ClosedPeriod - /** Aggregated financial totals for the line item. */ - totals: Totals - /** Discounts applied to this line item. */ - discounts?: InvoiceLineDiscounts - /** Credit applied to this line item. */ - credits_applied?: InvoiceLineCreditsApplied[] - /** External identifiers for this line item assigned by third-party systems. */ - external_references?: InvoiceLineExternalReferences - /** Reference to the subscription item that generated this line. */ - subscription?: SubscriptionReference - /** The rate card configuration snapshot used to price this line item. */ - rate_card: InvoiceLineRateCard - /** - * Detailed sub-lines that this line has been broken down into. - * - * Present when line has individual details. - */ - detailed_lines: InvoiceDetailedLineInput[] - /** Reference to the charge associated with this line item. */ - charge?: ChargeReference +export interface SubscriptionAddonRateCardInput { + /** The rate card. */ + rate_card: RateCardInput + /** The IDs of the subscription items that this rate card belongs to. */ + affected_subscription_item_ids: string[] } export interface PlanPhaseInput { @@ -5949,6 +5939,64 @@ export interface UpsertAddonRequestInput { rate_cards: RateCardInput[] } +export interface InvoiceStandardLineInput { + id: string + /** + * Display name of the resource. + * + * Between 1 and 256 characters. + */ + name: string + /** + * Optional description of the resource. + * + * Maximum 1024 characters. + */ + description?: string + labels?: Labels + /** An ISO-8601 timestamp representation of entity creation date. */ + created_at: string + /** An ISO-8601 timestamp representation of entity last update date. */ + updated_at: string + /** An ISO-8601 timestamp representation of entity deletion date. */ + deleted_at?: string + /** The type of charge this line item represents. */ + type: 'standard_line' + /** + * Indicates whether this line item's lifecycle is controlled by OpenMeter or + * manually overridden by the API user. + */ + lifecycle_controller: 'system' | 'manual' + /** + * The service period covered by this invoice, spanning the earliest line start to + * the latest line end across all of its lines. + * + * For an invoice with no lines the period is empty, which means `from` will be + * equal to `to`. + */ + service_period: ClosedPeriod + /** Aggregated financial totals for the line item. */ + totals: Totals + /** Discounts applied to this line item. */ + discounts?: InvoiceLineDiscounts + /** Credit applied to this line item. */ + credits_applied?: InvoiceLineCreditsApplied[] + /** External identifiers for this line item assigned by third-party systems. */ + external_references?: InvoiceLineExternalReferences + /** Reference to the subscription item that generated this line. */ + subscription?: SubscriptionReference + /** The rate card configuration snapshot used to price this line item. */ + rate_card: InvoiceLineRateCard + /** + * Detailed sub-lines that this line has been broken down into. + * + * Present when line has individual details. + */ + detailed_lines: InvoiceDetailedLineInput[] + /** Reference to the charge associated with this line item. */ + charge?: ChargeReference +} + export interface ProfileInput { id: string /** @@ -6035,6 +6083,51 @@ export interface UpsertBillingProfileRequestInput { default: boolean } +export interface SubscriptionAddonInput { + id: string + labels?: Labels + /** An ISO-8601 timestamp representation of entity creation date. */ + created_at: string + /** An ISO-8601 timestamp representation of entity last update date. */ + updated_at: string + /** An ISO-8601 timestamp representation of entity deletion date. */ + deleted_at?: string + /** + * Display name of the resource. + * + * Between 1 and 256 characters. + */ + name: string + /** + * Optional description of the resource. + * + * Maximum 1024 characters. + */ + description?: string + /** The add-on associated with the subscription. */ + addon: AddonReference + /** The quantity of the add-on. Always 1 for single instance add-ons. */ + quantity: number + /** + * An ISO-8601 timestamp representation of which point in time the quantity was + * resolved to. + */ + quantity_at: string + /** An ISO-8601 timestamp representation of the cadence start of the resource. */ + active_from: string + /** An ISO-8601 timestamp representation of the cadence end of the resource. */ + active_to?: string + /** + * The timing of the operation. After the create or update, a new entry will be + * created in the timeline. + */ + timing: 'immediate' | 'next_billing_cycle' | string + /** The timeline of the add-on. The returned periods are sorted and continuous. */ + timeline: SubscriptionAddonTimelineSegment[] + /** The rate cards of the add-on. */ + rate_cards: SubscriptionAddonRateCardInput[] +} + export interface PlanInput { id: string /** @@ -6181,6 +6274,16 @@ export interface ProfilePagePaginatedResponseInput { meta: PaginatedMeta } +export interface SubscriptionAddonPagePaginatedResponseInput { + data: SubscriptionAddonInput[] + meta: PaginatedMeta +} + +export interface PlanPagePaginatedResponseInput { + data: PlanInput[] + meta: PaginatedMeta +} + export interface InvoiceStandardInput { id: string /** @@ -6266,8 +6369,3 @@ export interface InvoiceStandardInput { */ lines?: InvoiceStandardLineInput[] } - -export interface PlanPagePaginatedResponseInput { - data: PlanInput[] - meta: PaginatedMeta -} diff --git a/api/spec/packages/aip-client-javascript/src/sdk/subscriptions.ts b/api/spec/packages/aip-client-javascript/src/sdk/subscriptions.ts index e60ee41aed..967495d3e2 100644 --- a/api/spec/packages/aip-client-javascript/src/sdk/subscriptions.ts +++ b/api/spec/packages/aip-client-javascript/src/sdk/subscriptions.ts @@ -7,6 +7,7 @@ import { cancelSubscription, unscheduleCancelation, changeSubscription, + createSubscriptionAddon, listSubscriptionAddons, getSubscriptionAddon, } from '../funcs/subscriptions.js' @@ -23,6 +24,8 @@ import type { UnscheduleCancelationResponse, ChangeSubscriptionRequest, ChangeSubscriptionResponse, + CreateSubscriptionAddonRequest, + CreateSubscriptionAddonResponse, ListSubscriptionAddonsRequest, ListSubscriptionAddonsResponse, GetSubscriptionAddonRequest, @@ -74,6 +77,13 @@ export class Subscriptions { return unwrap(await changeSubscription(this._client, request, options)) } + async createAddon( + request: CreateSubscriptionAddonRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await createSubscriptionAddon(this._client, request, options)) + } + async listAddons( request: ListSubscriptionAddonsRequest, options?: RequestOptions, diff --git a/api/spec/packages/aip/src/subscriptions/operations.tsp b/api/spec/packages/aip/src/subscriptions/operations.tsp index a779dbec1d..b2a0274836 100644 --- a/api/spec/packages/aip/src/subscriptions/operations.tsp +++ b/api/spec/packages/aip/src/subscriptions/operations.tsp @@ -169,7 +169,24 @@ interface SubscriptionsOperations { interface SubscriptionAddonOperations { /** - * List the addons of a subscription. + * Add add-on to a subscription. + */ + @post + @operationId("create-subscription-addon") + @summary("Create a new subscription add-on") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + create( + @path subscriptionId: Shared.ULID, + @body request: Shared.CreateRequest, + ): + | Shared.CreateResponse + | Common.ErrorResponses + | Common.NotFound + | Common.Conflict; + + /** + * List the add-ons of a subscription. */ @get @operationId("list-subscription-addons") diff --git a/api/spec/packages/aip/src/subscriptions/subscriptionaddon.tsp b/api/spec/packages/aip/src/subscriptions/subscriptionaddon.tsp index 181955acc8..9a8409f2a0 100644 --- a/api/spec/packages/aip/src/subscriptions/subscriptionaddon.tsp +++ b/api/spec/packages/aip/src/subscriptions/subscriptionaddon.tsp @@ -1,4 +1,5 @@ import "../productcatalog/addon.tsp"; +import "../productcatalog/ratecard.tsp"; namespace Subscriptions; @@ -8,7 +9,26 @@ namespace Subscriptions; #suppress "@openmeter/api-spec-aip/repeated-prefix-grouping" "active_from and active_to should not be grouped" @friendlyName("SubscriptionAddon") model SubscriptionAddon { - ...Shared.Resource; + ...OmitProperties; + + /** + * Display name of the resource. + * + * Between 1 and 256 characters. + */ + @visibility(Lifecycle.Read) + @minLength(1) + @maxLength(256) + name: string; + + /** + * Optional description of the resource. + * + * Maximum 1024 characters. + */ + @maxLength(1024) + @visibility(Lifecycle.Read) + description?: string; /** * The add-on associated with the subscription. @@ -43,4 +63,88 @@ model SubscriptionAddon { */ @visibility(Lifecycle.Read) active_to?: Shared.DateTime; + + /** + * The timing of the operation. After the create or update, a new entry will be + * created in the timeline. + */ + @summary("Timing") + @visibility(Lifecycle.Create, Lifecycle.Update) + timing: SubscriptionEditTiming; + + /** + * The timeline of the add-on. The returned periods are sorted and continuous. + */ + @visibility(Lifecycle.Read) + @summary("Timeline") + @example(#[ + #{ + quantity: 1, + active_from: Shared.DateTime.fromISO("2025-01-01T00:00:00Z"), + active_to: Shared.DateTime.fromISO("2025-01-02T00:00:00Z"), + }, + #{ + quantity: 0, + active_from: Shared.DateTime.fromISO("2025-01-02T00:00:00Z"), + active_to: Shared.DateTime.fromISO("2025-01-03T00:00:00Z"), + }, + #{ + quantity: 1, + active_from: Shared.DateTime.fromISO("2025-01-03T00:00:00Z"), + } + ]) + timeline: SubscriptionAddonTimelineSegment[]; + + /** + * The rate cards of the add-on. + */ + @visibility(Lifecycle.Read) + @summary("Rate cards") + rate_cards: SubscriptionAddonRateCard[]; +} + +/** + * A rate card for a subscription add-on. + */ +@friendlyName("SubscriptionAddonRateCard") +model SubscriptionAddonRateCard { + /** + * The rate card. + */ + @summary("Rate card") + rate_card: ProductCatalog.RateCard; + + /** + * The IDs of the subscription items that this rate card belongs to. + */ + @summary("Affected subscription item IDs") + @visibility(Lifecycle.Read) + affected_subscription_item_ids: Shared.ULID[]; +} + +/** + * A subscription add-on event. + */ +@friendlyName("SubscriptionAddonTimelineSegment") +model SubscriptionAddonTimelineSegment { + /** + * An ISO-8601 timestamp representation of the cadence start of the resource. + */ + @visibility(Lifecycle.Read) + active_from: Shared.DateTime; + + /** + * An ISO-8601 timestamp representation of the cadence end of the resource. + */ + @visibility(Lifecycle.Read) + active_to?: Shared.DateTime; + + /** + * The quantity of the add-on for the given period. + */ + @summary("Quantity") + @visibility(Lifecycle.Read) + @example(1) + @minValue(0) + quantity: integer; } diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 67dd3bd7d1..59486db178 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -3927,7 +3927,7 @@ type BillingRateCardTaxConfig struct { Behavior *BillingTaxBehavior `json:"behavior,omitempty"` // Code TaxCode reference. - Code TaxCodeReference `json:"code"` + Code TaxCodeReferenceItem `json:"code"` } // BillingSettlementMode Settlement mode for billing. @@ -5079,6 +5079,24 @@ type CreateResourceReference struct { Id ULID `json:"id"` } +// CreateSubscriptionAddonRequest SubscriptionAddon create request. +type CreateSubscriptionAddonRequest struct { + // Addon The add-on associated with the subscription. + Addon AddonReference `json:"addon"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Quantity The quantity of the add-on. Always 1 for single instance add-ons. + Quantity int `json:"quantity"` + + // Timing The timing of the operation. After the create or update, a new entry will be + // created in the timeline. + Timing BillingSubscriptionEditTiming `json:"timing"` +} + // CreateTaxCodeRequest TaxCode create request. type CreateTaxCodeRequest struct { // AppMappings Mapping of app types to tax codes. @@ -6290,7 +6308,7 @@ type SubscriptionAddon struct { ActiveTo *DateTime `json:"active_to,omitempty"` // Addon The add-on associated with the subscription. - Addon AddonReferenceItem `json:"addon"` + Addon AddonReference `json:"addon"` // CreatedAt An ISO-8601 timestamp representation of entity creation date. CreatedAt DateTime `json:"created_at"` @@ -6321,6 +6339,12 @@ type SubscriptionAddon struct { // resolved to. QuantityAt DateTime `json:"quantity_at"` + // RateCards The rate cards of the add-on. + RateCards []SubscriptionAddonRateCard `json:"rate_cards"` + + // Timeline The timeline of the add-on. The returned periods are sorted and continuous. + Timeline []SubscriptionAddonTimelineSegment `json:"timeline"` + // UpdatedAt An ISO-8601 timestamp representation of entity last update date. UpdatedAt DateTime `json:"updated_at"` } @@ -6333,6 +6357,27 @@ type SubscriptionAddonPagePaginatedResponse struct { Meta PaginatedMeta `json:"meta"` } +// SubscriptionAddonRateCard A rate card for a subscription add-on. +type SubscriptionAddonRateCard struct { + // AffectedSubscriptionItemIds The IDs of the subscription items that this rate card belongs to. + AffectedSubscriptionItemIds []ULID `json:"affected_subscription_item_ids"` + + // RateCard The rate card. + RateCard BillingRateCard `json:"rate_card"` +} + +// SubscriptionAddonTimelineSegment A subscription add-on event. +type SubscriptionAddonTimelineSegment struct { + // ActiveFrom An ISO-8601 timestamp representation of the cadence start of the resource. + ActiveFrom DateTime `json:"active_from"` + + // ActiveTo An ISO-8601 timestamp representation of the cadence end of the resource. + ActiveTo *DateTime `json:"active_to,omitempty"` + + // Quantity The quantity of the add-on for the given period. + Quantity int `json:"quantity"` +} + // SubscriptionPagePaginatedResponse Page paginated response. type SubscriptionPagePaginatedResponse struct { Data []BillingSubscription `json:"data"` @@ -6355,6 +6400,12 @@ type TaxCodeReference struct { Id ULID `json:"id"` } +// TaxCodeReferenceItem TaxCode reference. +type TaxCodeReferenceItem struct { + // Id ULID (Universally Unique Lexicographically Sortable Identifier). + Id ULID `json:"id"` +} + // ULID ULID (Universally Unique Lexicographically Sortable Identifier). type ULID = string @@ -7049,6 +7100,9 @@ type UpdateBillingProfileJSONRequestBody = UpsertBillingProfileRequest // CreateSubscriptionJSONRequestBody defines body for CreateSubscription for application/json ContentType. type CreateSubscriptionJSONRequestBody = BillingSubscriptionCreate +// CreateSubscriptionAddonJSONRequestBody defines body for CreateSubscriptionAddon for application/json ContentType. +type CreateSubscriptionAddonJSONRequestBody = CreateSubscriptionAddonRequest + // CancelSubscriptionJSONRequestBody defines body for CancelSubscription for application/json ContentType. type CancelSubscriptionJSONRequestBody = BillingSubscriptionCancel @@ -8565,6 +8619,9 @@ type ServerInterface interface { // List subscription addons // (GET /openmeter/subscriptions/{subscriptionId}/addons) ListSubscriptionAddons(w http.ResponseWriter, r *http.Request, subscriptionId ULID, params ListSubscriptionAddonsParams) + // Create a new subscription add-on + // (POST /openmeter/subscriptions/{subscriptionId}/addons) + CreateSubscriptionAddon(w http.ResponseWriter, r *http.Request, subscriptionId ULID) // Get add-on association for subscription // (GET /openmeter/subscriptions/{subscriptionId}/addons/{subscriptionAddonId}) GetSubscriptionAddon(w http.ResponseWriter, r *http.Request, subscriptionId ULID, subscriptionAddonId ULID) @@ -9060,6 +9117,12 @@ func (_ Unimplemented) ListSubscriptionAddons(w http.ResponseWriter, r *http.Req w.WriteHeader(http.StatusNotImplemented) } +// Create a new subscription add-on +// (POST /openmeter/subscriptions/{subscriptionId}/addons) +func (_ Unimplemented) CreateSubscriptionAddon(w http.ResponseWriter, r *http.Request, subscriptionId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + // Get add-on association for subscription // (GET /openmeter/subscriptions/{subscriptionId}/addons/{subscriptionAddonId}) func (_ Unimplemented) GetSubscriptionAddon(w http.ResponseWriter, r *http.Request, subscriptionId ULID, subscriptionAddonId ULID) { @@ -11280,6 +11343,31 @@ func (siw *ServerInterfaceWrapper) ListSubscriptionAddons(w http.ResponseWriter, handler.ServeHTTP(w, r) } +// CreateSubscriptionAddon operation middleware +func (siw *ServerInterfaceWrapper) CreateSubscriptionAddon(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "subscriptionId" ------------- + var subscriptionId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "subscriptionId", chi.URLParam(r, "subscriptionId"), &subscriptionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subscriptionId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateSubscriptionAddon(w, r, subscriptionId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + // GetSubscriptionAddon operation middleware func (siw *ServerInterfaceWrapper) GetSubscriptionAddon(w http.ResponseWriter, r *http.Request) { @@ -11857,6 +11945,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/openmeter/subscriptions/{subscriptionId}/addons", wrapper.ListSubscriptionAddons) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/openmeter/subscriptions/{subscriptionId}/addons", wrapper.CreateSubscriptionAddon) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/openmeter/subscriptions/{subscriptionId}/addons/{subscriptionAddonId}", wrapper.GetSubscriptionAddon) }) @@ -12221,439 +12312,447 @@ var swaggerSpec = []string{ "FwkEln2ymtxEDdEntoM6kdkrl7YF/5vuMhUVaJugTkWHuhd+2sIq/HEMVt9Ei5MPAaqd0ivkwg9aviA/", "ymoOTOLAfzIMsFPLFwTBYIGIWEtxhSQyJKdY1T40Soc9lt2TCyHAYwzW64vy7Oal98nSUPLo52iVptw/", "s9XDof8tVHnHs56Rt65jPeh9yZolHzBvPOBbSFn/1TA6V9HaPQrz7SpsX2GpsHdp8k2O02BI0VaWxVzI", - "W2EbodUNts4IcD8U1Upue0boWMwOeB2VHWT86e+wkTQcElgusRjozbqkd9+7EUFjgb6RsJU0rJMZqx/f", - "g5NGTYBIafh2yr99oV9I1n9Vqz5W4CCYO7x4jW33lBZsuVJG1p2Nullaypvv0A9lGDdWKdOSKBhPbWQe", - "Bt5akcgJlRQ/2EiFKaQuyBF/jvBD/H1xvzGn4Q6C6s0efg9eBZbv7lAD2MYLPjCIPUjugrKG9V4d/6LZ", - "5G3EfQmLrXSXzhPJWhb3VNOoijw0k7rQN4yZziVHydI2sqe6GSIbJol+pebC+z4rf+o2yRgU78EkfuVY", - "G4ZnQwEZuMRreVuJHArjXxOaJWUGZW+JeYXQItrA6WjSKc2YSGlBNlLoNXlyjBUWQLGCPx28IMtnR8++", - "OTw6Pjw6vjw6egH//78tzehaSzsqBL9hhaLFljxJ6ba6MLZ1aJkfwJLwZ6wA9cR9c+hzwElKtwc9RS2+", - "r+H7yyjuhntc3FOEgsdrFaqgGve+s4FONfRLiL38dQIf84yK+zor8Pt2nBO5YFCBtt5e0heqRgsk9Zn5", - "nS03cIkHK4H025J37r+qUUgER1Q3Gvpkv6AiPrHozu6wzpAsTpu82Z/G+2FywikVCctGR3X+rWRKowYI", - "E2BVF9UrRWi+2Sd8M4T2Vcr1JU7TcOtvNizlVLeLi52toGCFrdaaRvrSKrsHo5m6abLtbLCodQpJyJ+C", - "wqoujnqUw34TcpgBrnHzpu2r55MfLePyMUFNrHpZyRZIqoVZIkJiYX2BqGfB3MM04bGgpYsVaG1khes/", - "cFzpUEigxACHrJqayBf88wPb+uxnd0CQbXol9Ro4bO3Y5uLsZa33Sb0ScE02fICi9QYNQZX6zx0PsIkY", - "8dxHoNy3EllVpbVDbniQy3pfYivQDziqfw+6E9jqRb6eO19ZLarFO2qTkbPVXMQPlPScZyAf32M1Xo+l", - "YGMh5NhDVGwHomsuPi982cDLWlXC6PYbdYLjKIiyEKw4PBexSsO9sPUWIWw91ked6FN0oocQdmv3CH7o", - "iF7EsjjTKoYP/+CauGj6wRWCnZHXspiLDgkU7xt1wYI3cCAkZWC3dM2gg+/not4mte02cGKIpe0eT+/H", - "iNp7pgy5YeNkbtsK/pNOsrfPgK4TIhtJVp0atqNhdw8CgmC3tvBrhA4FERcelNaBInYshEOPEUj4J5io", - "ndhToBb1qCk9akqPmtKjpvSoKT1qSo+a0qOm9KgpfTGa0g4FYaBAGahK7VCj8PIagAjqHDZJPG1qSSqq", - "Jp2tkKp7P0Jd83IdKso8l1gpaxu5rNT2MXCXzwiBGHZXb2cdejNGpQfEcfJKlJud0f2B5LsLxzDfYDzb", - "/HW/p6nNHwXJ3SDSYj1A91yEjhib/87utO+SC5LygIlsa6C5MKMbcnZY4DPAd3udvgjQcNP7lmqoIa5i", - "xrUWsUHPKUyN8t0Z60Rfs43NjLbex2qWX0DsHOyaDWFGvpnv23UCG078goJLtV1sx9AXYQDA4QbN0eyx", - "PzOstd4D74vbgNyubZ0ZmMw8kdYtkTIfjVEtir57Fou99xHMxmdrjMbLNZCXdPVJqF3aqjugoyG+KQAX", - "/j+dX3lof4CLsGbXCAJiO89XzwCn+UrV6nPZNmnWQFHv8c6VKm2L9wtdlIkuC+ioTSHZ+RAygs3rYrDI", - "MiwVtsTyqxCRXc8VrKV+2GIwB5AECFA6qJWmW0Wg+3pNs8f6AmT5gW2XIJC4pMErltBSMZtKCGIfLard", - "23nNl74UWUo1/Y13025dqscW1g/XwjqMqW9rLvSOuBh9WwMWGHK5CRtNIgff+p780OVY2ULJVR1kX7gb", - "JOyUpXPhMmxjlcqtFA7ipPvvHopjA/3HMibIVkiZMmKulYiLaIXQhc3jjdDTN/gL5LjkOdAPTBhzk48t", - "/Wr3cpLnduqwvc6JXSJcgXjg2nW+Pv96oo/VP3+R6p+/l6I7X2SQ5MBGeTVC9L6XqjcoyDi6OIiktWmk", - "S3MbW9MR0tg6iydbcrd2Yp8jelw1y1DV6rPbgVVtoGimp52sXb8n3Z1Y57ccrLDrUPZIq7tAWdWxp0pj", - "rnLtVH9u3ajzqGXZvZ/uEgmsvbLyhUUKJ4bSgncm+sP004ERtj5ZPefVzWe7BFqEfKV8nWk3lVuweaxX", - "Qfogs36Yxf4FWf2JOp/OmZ8KMZcXLMGOQ7FuyG5UULW1OtJapbe+DzFwveB7PD0P/wWOHwI0ftoBaOvH", - "5usbBl47a/N9l+QW1AP1/QPAjr90y2InXv/PBU+XDSN/8KmmH5iaC4MCBhn+1mi3RAzP8CuoF6FkfAql", - "eZaRWy6UWXguWkP5tZAFS6MkJ0DXvgko/cfn8Xb2MgaAs4bspGHtGz+OqO28+8208K48fc8OKlUUdKO8", - "YIH5mrWXC8X9QMT49vkenGAw6b+oHureedVGwcN5huLJ9i0mPztkvX+y1jpXL54+TWWiZv6Gbp5qemf+", - "7xC4/EHdmq/vknRxfIT/Dwola4PVyYvJ/wk/zefpP/748Z8nD4PAs5oSfdrJzgPTghcTiJDFhmZQ9y68", - "MLfCRX/Jgl9z4XV1vZ2LVCalK1IQXJCvn+0UQR3QUtNsbMIzDnIFQl3EzWaDLU6MQJZlxMXIQNQMJkQ7", - "FulF6wfukaMNnBhJ15TqfegXvWNqGtRwpfVU7j7TbNVex0YNLWDB+4XeJoBDuxB+VVp6UR2Wi1jqTai0", - "QU0I3keXMf+Q8KbMvGYQtQpZXq+df7KG99A/2gu/Hevh98f1S+7AL9oHqi/6VwELW114C9FDgAwF5W2l", - "gDS1BmGZty59lfHeBf8rB6Y9ocYuvM3roXeRGQC3LQPdiJ2cid6dPDD89dqKIcHrgveyAeV9wrcCttFH", - "Dm306SdQw0uEvKl/Io2sY73rNnW9lSaBbROAJklz+Oth10Hh2j1a0HUVtUUTdEGFWslio0hBb30hr6Bv", - "HBdaOnZ4iFcdC11a8hjU0Z+LsOYOM8eHK5ETe8ms0aGqAJmxG5Z51w/dVGXyEirm4gqVVXxVKV+BcqJd", - "uzto5YrVdjFUD2UrF/ECJQWxxx12yiN//v4FMSgDqL7DIrjTAEGvaaJl8d0x+9N0LgpZipSL6+8Sxs3m", - "4bLlGd0arH43n/z5+/mkWkaxRIrULbSWZaEGrfX1t0dHAxaDCXG9U6k0+Rfy7Oh/IxtaXHMRruOqA8d2", - "NXtmhps7BX0HN+ZMgfp+YEINRMy3A2B9Y+Cci5tjYl7XDc3Me7Rn8hLrHEOd6ScWWM6Kg4F7qEaQf8Gm", - "pDgTvt3vjiG8+BzrIIe/TH2PvXNWnH+4PohsF7IU6svVB01Jc+fkX0gThoNYDS4762IF094bqYTZfDvY", - "qvMjBrW5/dmm/a9lQZa41eULsqya930HL/9pC9nLmR/mTiQ28H/9z+jIuXhjo2IpyaXi0IFZSHEINWuB", - "sjeKeznALZJQdjLXalGKWCHLE7IuN1QATYJuEGBgr5JdPJxQ5dqrJ9a9rWz7UfRG+3DeXBaG80CzzCnB", - "R27+174/859v7IuZTw5gn85rYW0b1hs9JULiyggWRFGKtNk61pnzqy9j7lV/lp9Q8/wnP0eHabjger1h", - "mifV1YHy4XleNd+O8IhtxzFWMEONW5bwKp7TpucfRVNPfFXllCV8QzND4G2jqIJBt0KUANxjxEMQ2ZZs", - "GBVcXK/KDIP33BcuHGA+EVKw+aReifKIPIEPzX/frmXGvJu41i/krZvM74U8qUN4EIkLnU4cEJ9wdG7p", - "IAzUFTgwG4p2LPB7h8jQhpDXahC7hWfjrTmA0hBHZhnyREg/7cGMhFWhoTCgIjS7pVvlK94iphg0ZIGB", - "LD1oP/23IaQtqay6RtMIJR0kNP0Uvp6BF9/Z1y2mahcfpY9QLrKNT22grSeyL7HKflgh1ePbWtKSJt1z", - "pAfEFvK//r+5OGZ/It+RP39/gAG3AS1+41oB7LVCIpWei//1P8nx7Bn5Dv5ZEysOwggC3NNkOnHL90UP", - "dNzcKPY//Z6+DV65Gw9BHXYSF69cEaza1XWFifEKz0Xj8noqhMoIkp4Kp+7MrUCwfEEAHFLmbnkIMbX0", - "gDzR25wnFLmU7ddwqPQ2804Ze8qrTMrCz5Ya3uWL37MbLkvl5sTv1zRbLcrcj/CL04Ipv/4Uw6iOZt9U", - "eC/zma23KhjWWXU//WvjDBADzvlE1SGvVzxEFEymEwB+Mp1YoCZTJFM9N+avYS/F8d3MWq0SY6bcLGPJ", - "XjzUAXfqp7iwy3QlcPoPI70bfcvFj9NPaF7oYPIOgx0gVYb6foj2bWTo4LFNbHZA45rk9MOyT0tDB8cl", - "vdsBg6Z3vev3uG3aF+Ik49ci3jwBWIz72RbHxWHQ9J6RnOFDzLjAGFvLW6iXVVvsuJ4+cLvmyRqEH4V+", - "/VuGreB9chuFhF4X2Am11+fCB3w2Enls73HsVUGeIMH1/Y0AwAMIPBOp/3kubPck+/uu3g2YV7mz80IP", - "ok/cDNOJqtW33HfCelb0/bRlGL7cp07n0dHXa2HI+L0I8ICJfRSCsjX3gxdhNO+xT6JF4H3e9OjK92/d", - "yHNbXL6j8r3Ly7YPxCmafiuzYbWpg++DMHGH//cD60+3trvfuX9Cbdhxkz/4+e+J+BrxeP+pDZV6RIVx", - "yHUT1WQJj8G1vEXlBH7rQhl4MoVP2Iu4MEOu9YkiUcUBawrqPzxCG+T1/pkkClTYmKJmZpicXx7/W0RJ", - "5tBVJXFNJaBZi2vUB6FVGXUqlD8DFwnfAmcuamFW1V3zgd4oOK/KDJqqJDY/yepT9SQlRdb0BoS1jF+v", - "dbatbN9B5gRArepBBefHL8O+JbYDRzRgwOnf0avmbu3uq96WQMfd9DMrldRkMdrqgh65vaWWCyuW1M47", - "FqjzVxdNL4kZt6HaqGHZFjNcankvVXWTtKArjUyBrKkiOVUuCq7VbQe+DVhPcPuOXsZ7StbZiFsem5N+", - "YDmUDYFpXaVT8+mGipJmxCiB7HbPwwdMXhdMgePKYnonCi8wIigY6g/pyjbRifUI/y9RdIW3fcFEWnXa", - "2C9Xun0bwxRVEW2c4g7I7GJBE13SzB1gc++nUuhCZkh5Q9gPLSFQa1loJoycg/2i3fMESRx9WLP4s+ON", - "BzDqzfXtssUM2+pyLTdXpI02LbUWhAZJqzILUBTB2wAV32qIGOhxEr7FPQnIeUyZbBOQIJ+7onMbptcy", - "haIIAM9c1KhDnxlhgWPjIkd7icYzrxlOYOlFbeXdokgbkgFyiVfPRWrp7q+KdMVE2iOfPASyzZK+IUfs", - "uqYlWwALaFDxr/vJOHINVMVDcs4VSUs2I3/lek2U3ATGD5mVyOy5/kqByXIugubmAbVv7bBB9xG2QVz/", - "/q5QdW/2uhq7TATRVzHKCNVHYqb1izDSuNV+PW2DQRu5+1oPBmxnpAGhbyMDrkBoXBufITha0LOPcPEJ", - "Pan7elFbw1oQE1x2eD+969hAXon+mB6NuyK0ioh9Uo9Vfwph7acyZWfp8gCbptoMjCDnQtM7Vy4RexW6", - "AHQMcKKCCEkyKa5ZAWIXxtDZwoUsfWHDb6+p4H+nWHwxrW0wrUz4XCjNaDojr+640jZoHGKVXU3HDd3a", - "CPwr6J0mb1g6nQtIAXDZH0vfo9KIB9uq2AloZLYh9E7p8hV8VwnnCG0VmGs5CSbDtuqp0DyHkpXEEsXp", - "XICnw2ZQeHkJZ7EwTd10VY08csOpG3RJ7+KiPhMrWSSNPcXD8vHLPfby1zqcYcY8dkbDWjFCapS8Q2Ov", - "qFL4uV7LUjfXn82FhQw7NaogtCpnhYFATeGSO3Q6nNA8d8WgVFhFbS4gyxovfyaTYJtgR8aXkVOetopx", - "NPEbE5FDW31MPEbSeE7h/ww3YemexUDPocmkm4QUdpYIUaKaQpWNEZnICGZlJazSijcMZ+ubxG/tjfm4", - "ycsBHjtRjISfZlKx9LwyjQ7HyYmTceBJufJIcK5AlIzikFLNlLV1eJmI+4gAHz7pIgCD0XeNn5iI9LZb", - "FXJzLwmol35xb8qBvo9t0OMw1yWvZ0fPvnZ1MY9fwP+fHR0d/7daEhctNIbJ3tsGoM7uDvC78DoK+Fei", - "3WsVjgK2E71nUqwynuhXRTEqW5IqhkOMKNPk/5pyMOlVsLtl0OintDMCVV98kOL6hS5owl4cP/v6+Tff", - "/uGPfzqqt6XxHz8/+lNFdrqWcQbm6leX9QP/C7MyBWk/z4/+FHMgvgf8KP09VVx9/tTKQforESxZCl1s", - "45lJP59d/ES+Pv7228PjKvnq9vZ2xpWcyeL6KVfyEH5PcBrMwJqt9SY7IDTL1/TwGbG/GXkrZTNXllbf", - "ysOMaYzzgA9sUQtIAcmUDFi27XV6wwSH9Mba+3p30Ui3r2U6PatlfP18cvjf3v/jGaZ7NVVSrIt8kqZS", - "2P4wYym4GbqzODIWbU62o6XrUzvwtLspsZsbBVBLu2iaHlohwFdAtSB8JqUiLGUZmYoP6D6zY3uy8cNP", - "6jhRM3KK3oj5RHFxnbH5xEj5cxd/xeaTWsLpPcx2v1VKT6AyLmQLKrbhhyVWKkLcYphCkMZuswy34bUg", - "Z+EncxHUuvM1PMkSi2otm9VAcQ6M8YWe9WZxV0OqqvyJgZ4hJr/oYh3QnzqhRao6gu1c3oVqP8ExrMG1", - "Ng4r6bz1c7fK5jQov63OgTU46k9sWhGh2m6iDALImaskhYr0fuSxPsdOOmnUo/GlrnDykzz3efiqu0hH", - "ZvNy7fUHzZCrZuGGGZJJ72rtcHlZ5yN4tKwpwE8QdWX9+oT3y3x+KqzTN7oOWlcrBgM8JrcJTRNdKxjn", - "DN2+oF3smhjS6ap9GjBvw2DLfZxsHZBedUVjVk5BC1EXSfAIDICc4nurLno3LUD9+nVG9Wu2Jykwg8mK", - "MesP2k0LIOlngblwlRNzOGqdxOMytOMUAVM1fcadc9K5cvYAa61cT3yAQZWrYTs2SqpmQOi/AFYJrUAj", - "VyyT5iJoGeuyYEH5ON1DBB0je3ai6zMTO33K6HhJPHwBPsG7Ay9B7mwj7L6FoGCq6cRmaC6stBhJTLMp", - "nI16UsHctgdR5e0KF3tth6OA0MIOOKGtl/0hbrGZf0pKga+m5dHvvkWvyyxrfFwFebP7qqB2aWu1YuW0", - "sEh5gaXXudBMpHiaV1X6Qwipi7mh+stlttaNutCs2OwhjfGEWS/YpZmgjWfnxjTzu/2s6tyh1h8g+Nx6", - "A208S82nNBpSJ2r70I7T2nQd8fWeQ9QdWp1X9zw+ALsYPNhLw2Ls/IY1n1gibyCXyaUwteC9aL2yX7zZ", - "QicuGx9WRaju3XN52fJYdgLV+nRYrHJ9MhdDYZ7BYsVYWHn70lbua9WPBNV74ZX3RU8Z40rDD1oWtbYS", - "+XhnpIOVL9s6ZkCcW1e9fakaVKf7lU87BcNd0msgtjZC0Jy3rcJIX/iEP6Ou59ktMU8npaLXbHFFVXeG", - "Rjgeun58b752U3xq/kQPdLtCHXYA9j7SbqlT1u89qfbkI4tzmPGHgOSh2saj7P4blt0dp+8T2192iexl", - "6zI9pPj+KK0/SusjpfWCJ2w/Mb1TxuUJ65NoYeij/Poov7bk11C8+f2JsCH9dw+zRyp1UQD7CTl++E7Z", - "xj+mxb3F+gwtNg+eEQMcdNCoYrihJMYVQM/VXHgII20FufadAyuqEHQrAyrqvlBMg0dzKeTt0reEg7A+", - "cOmsONUjazwPEKXMrA0/fFUryW105tyI91WkCqYGv6NfzILQ7hpd7dvC0HMtoYbeSfofpYJ6f3vezsYs", - "A83994GbPxdUQHc6AMEXidxLjt4jDMPe+usaFGr26P7bW8aJk+uATtu7s+tOw8V4zTMDxFjnFY6qXhre", - "LTjkSEgl8oFIqMAPfMN1a4aqORFPnI6gLCn0/4YgKZ8JayOXa7OEWbaYobx1o2vhUz9PuMhLvcBia5Pp", - "RJY6+Pf7gRELjSYsrcCE3WdxXhbJHl0I3TC4W3lgHVa1xxehMTeUZ/SKZ1xvF7nM+D6kAAlbMNM5TlTP", - "fJRi4VoKdac8YrN5XxM5kRtGLIw2SymRQpWb3BcbDEuVLIM1lrMHsROcNoha7jFfEdWcFVBGcGH40QJY", - "3b5U3CHveHY0iYk7VmLIWeGeDxT38xXFXLJBFFbbA2QZgRe6HRzNvjnCaICQb5hf/vn46Gh2dITNPLB+", - "rstpNL9+Y36Ermi2jpNrWn4N990QDCrIketoW5d9yNHUwD8XyxWWhrIpTd9B0SifR9I8++PZ0SzSytce", - "/yBC+Cl8/c9Ib75Ilm4R8Zvh6Owu5wVTVWblMDTYTMaX/e6u1KmPzSTMGuOxILRrG7IbVuCvXFxbAdyz", - "3wGH1GbbYPcKH8qeBPy1rZ5n8wdbm3/taudh2mudr3zJtiJZcL2t5Vcdt4qFvizo7SFUxnMD6gggP8hb", - "VrhMNshPWvPrNSv897MwaZYLffwtAmuuuVnx6Ahgtf+MFffMQ9lgzMOOiBcRFzTqqTZWgtgrVcVXoQ1Y", - "kSf1y+bKnWJJv4PZvhacFqjjbDlOCsXTMPcEagm71vPYR8h1kln63eEe1DSSj+l70gT91rUkTCgoGC6L", - "giWaFOyGCbCuJPJacCxAjr3Cayp7EA/ZEFFrPdHQzj4lfAVRy1/Zju4MBeTrTF4B7fQplmYgsFzsmNWh", - "FzTIw96Kwr5d0OLHResqAyQVdZ8CGXcI07nAHgnmlDG5EM8+28LhsxSX/XW6rzUdKi6F0qf5zvZrvoUn", - "5rSQQT24BkDysccyGjnJHldmKGHcl0D+mlMNOcmYUFOzObXbB08nd4fX8rAj68UvBXPtKQ7W5tidB2Nx", - "MdLkgrPvlLdetsWs/5+9919uG0f2R18FV7VVm5yVZDvJ/MqprS2Pk8xkZzLxGSc7tTtKyTAJSTihAA5B", - "2tak8s99ifsS9y3um9wn+Ra6ARAkQYmSJduxXbW144gk0AC6G+hG96dLj9jae2V4x3Zxw/UOyOvck19a", - "R1D3szPKmOGRyFva6xNVRDNCFRn13p+QFzJJaGZzS14WmcQslXXDqBfzM5mER4XPVo4rPBDjZzUt4LBC", - "Q/nLqAdC5IaEA/r//+//1zzQQ4OBbeAQaivNZdkdYxI2ZXQb0dAxkMBUOl8rtQk+WB5EYJotlwnJGpL3", - "fq1uBOC3UM6B2ALX2TU4DQI0mtH0lxN7y+IPOtQltsUC70R94jTjc5otxmxucoSDN8X6FQKvtPJm9e4Y", - "PngJbYZuBOECkeZY52yTsE4rqRAEdOg11ORUWz04l8T2WKK9AyUubtuKvy3TeA44PAXomMr9I3xV6bY9", - "R6tdWZnQjw0zLWydnxWq6suQqSvKEixndyYycweVh5YeKW1dgLLesw3uaZZ7BnnECCLA7uDRzLAXVyNR", - "YqS428+qm99vWeU055HPcVgkKStpvTX1za2TdW0RNouAxQ5UKHXHcWfKskGBtbhUXqvCpfcbMuoh0pc5", - "X1Ay4Zcsrn7XJzIbiVEvSeajnhb5RMqPpEixUbi61pP1889v8JeY5lQvZ7mmyArTTBbp4GwBRrQRtFpw", - "Ana4mVIwTLmJSkAmWemwnU4zNl0zmh6aPvS+bEm0Kt8wddIlVDOxvgyYweGtCTWcM6G4FEvuCT8FPquQ", - "+guds5j88+TtL8c0nxF2mQK4pxTgG2WXuSbJ+HFkkeqtxXjUHMfhNqPZDcb0E1sod7tgMq9N/qLiKsfa", - "HADBINCVDk8LEbNMRTJjtXlwF4JuKH8Zukr0dRYEUsbtcUFIql1ZU7LRFs/yFr8K5JBmcp7mvZW6BFpX", - "461i1UBciuI27R1F2Jg6ln7sFzTJpEjgnvYjT4lMYveoUQu+D5VvZ1zlMoOKJPgm3N/aUpbD69n8bsdG", - "AFw9tlHrTXoCIlKXELxYc3LBxZQpvbvC1P5VgYg4rbtwmD3uPfO9zIgq5n1Cz6d9MueiDwOcayOk5FBl", - "KjGbalkZoSPhW73mmj2lmbl3tO86T6wJa4uwCmjZcr9KfPW20JrWiLYG345ERWvaeXBTWSvSXorVX4bu", - "bn8DQxrD1/zNoCL+7RvUcULFFbBU3OerNyr91pqoIatOcxbXIhCanSa0gqVyCK/ekr1KC8VYUzhO17sv", - "WY4yoqfkIytv/hMqCHRQD+Uz84bhC6oWv2BvcSrh4fr7sr2NDvX0cmxrToW3I3PX5NW8gzBFn2Irx4bE", - "EtsWlltL4UggpsvAQmlY1BdMuZ9wlsROfk11QhRfQH9pfDYSF5U6hoVI+JxXinDViTK+sPKizJ766aX7", - "KFgc7/YGbqHwNhl3uV7ZXKV0dtlFNGZinVj6bnfo1nVnmje7kF+SoFC4uXDVUDXfVz/efTaQjyJVJ+Z2", - "OuRuGFMJtcVVEJVAF4bxlOyrtnDmncFTApFvwVIq9wZlMDFNeT4Ems3oPDW2fAXxlByaDUpd8DyajYSM", - "oiJTxipySI1Qr6aWlDckhzlJGAUbiplmoK4saq91UZy02jm2m9uci9f4rae/j8sx9poYgGkmx1qniOm4", - "K8qvxSRKMznATwGqEr/2YGJqIn1cvm57CgLHth4Tvdv1uh51q9yu2Zu3t2tftmulVrq/mtodU0qWLdr7", - "n1+/aAySx0vINt1utidZmjuAYY1N2nFAUKzvWnNzmoLhDX4FG2Gh1uVZQ9ZhmpqmfQiyQ9OF3wNxxDUZ", - "+F64lm/vEctYcD4DtTBzzPPvsUzrulxcBgHaOq9ni8p9ezD2OtlK+suRiZn27XEMlWYxyfh0lhMhL4bk", - "Bcv4uVZ/2uSIEkYzFwqDEY1YFmIB6i/hU44R3bAwGdNzjKUhsiJhtYpABxD2C+XiaPxWJAtUy1vNMmk0", - "bYpjbX0KIYTvTDOlDXI9K2BihczJguX+/A7Ja3RiKTOTI0EvKIctxAbjw1QDT8jM3ICQSQF3GGUOV0xz", - "VodNDs9pW5xzOSF9j798HIIKg/ZaRQDDBW89bHBJ6w0BBwMB7zIqFAXBuOp0HRWZgnLNu5wwj95Npw3p", - "3GzOqih0a17bSMFymi1cMoLQh16bKbRE2brOOimHqs7qbknWpsL02l+RiFCLi6tvKxljFiT699cnb589", - "OfimHYhaPx3Y7nwk6pGoxsY5CGq//corbSjUUDOkDYb6RXW/flrZrZ8GcKiftuBQG0puvw7yXAA3oIBK", - "OQwUR4KWTSUnCpdmjSGndMq6C7ueyQaN0MRy4o5NL7WsQJ6F0Ife//raRpnAGwQ68MLnU5rPesFUzhXN", - "6Rc6tybY5fLWoNp+S2uiSMzZDk3TQCwRltZf2oOrv79pL4r/GVApxkpjMbRL4CVPig/2XUvoY2msuPkC", - "ZsgbSjsPlKz4PwXLFpYd1pDhFyxn2ZwLpoynHUi3gVVlzbJckozlGWfnoSK0Za21wHxUF1W3rUWmX1aV", - "cT4orR9c7U6ulyej5ro+sAiIy7W828pKr+zaYK126DvMABCz6+4BsMStd0edMjiB+PHTzo8eTGM1sWBf", - "gK5GQm9KV9t4281cOy7i9tp8Oy5eoFkU49dXR0+fPv2uPIjkUiZqyFk+gbOIPnPsZZNIv/SYoOLSC6pt", - "nEHO586VzMVIvH931LlcjNOBriXPeWeoIprwwTs+D2Ks2FG94iyJMY0OVkIs1gqh6K9YvBrkyB9bCdP4", - "F9yQox2eLAj7o6CJIlN+zgT59dXRAEZfznaJRMIFgVn+3O9N8y2SAoWtvaxeiN5Zkxy2Q3pkhpNkt9T1", - "aEu2PFUJU2rTeUq2PU8lMVeZpEDpoTasCOkPW4sv6IFHtp/H5v4YQjqG5DBJSClD2hYZCWkcpf9t88EI", - "u6QgCVKwEjTCllKfpzTjqlr2xc4MAfEnRv6riUEmJ3eID91cei8N+FwbRHiC1qZNb8rzWXEGJZlkygRs", - "yVyWf+/RlO+dP92z+b564kLR8Q1VG3gJ7+U6XslR52wFa1mMhE2GczhAvurdP/iOsmf7k8GzJ199O/hm", - "cvDV4Ltn3x0Mvn32ZPLsCf0m/ppGq32ytqaX7ckST34KQ/eZANe1q8VFNDWgE8RkUUCkhQtXJBKKDQI8", - "GrVMkwXysEBnxNuC4uuKBsVMjIMBrXAewIafM2YJuzn6oPcV9N18AaU1Mt/xEBRyKD/E3z/E39/L+Pt+", - "r0jjG9SB4JtBEtrUTNNo6btUx1J/VwbSnkFgllQvJ/gkfmXKlDZaYwPCjzCAwsoELMgfusl2o7UFu04/", - "Jpm86H5V3RiFvAgZt1sLEz/xy5nqQXJkXS4xgntLhUdflkVHG52ErO5OC2yqEK1zvMjkhY3Yz9Zc6vX0", - "zCpgw3laANKO7tBcfTxChf3//T/EqY3HQ/JLkSQI4mGjgwxCh7sOHAmHlgEH8UjOz6yfWE7KoxMw4UoP", - "4/XF3nljN1kxeTAN8wX8TuZMwfwgpJg3b3o+AMiZXaYJ5QJLdS/KPiJIeNBzdgb1rJRMzg1K8k7SYrA+", - "jm3Ii+7X+7aJOddK/ZxlQ3JqUiwNpolNwxzz+BRNJK1ds3MW1xayIR67q3IMVt1ZEX1k+fLB7K5S8Vok", - "gBxtS1SrybJlloVXP7mhwpCAPqqMSgBZWf+4wmtL1F3tnLjuduaCNCWh5gzRODr6EIr1C50VUJLLZxbU", - "NNrcJyArb2j6OmdNnAvnUdBkpmmysI4LT4wMJzjX+Jxe+pTAJUflwOp9a8cRmOaNrIxAOdYXFQJhIHae", - "rbOfK3+mq8R29+Eavri9vnlr+t+IT96llRvGB3bbLLn82rzyr2R2xuOYiR3XPXf9bKvw+dNw4fNKP2tV", - "Pn/aVvn8B5ZXQvmOaUbnyvP1rwt0S6TJTNC6fIq1JkvgIxNOtY0y16j4vKuJl5d0iVN1acRhswDHhjSs", - "0/2krNExJG8B3BeuZItMeHChqczyiUy4NN7ekdCm8Sm75CpXf4eFOPU+hIj1QtgIRBaXUx68kfxBCrZj", - "6dBdbEkwDvbDgmG7WEcmDvZbZUIfeIQm1WiswyiyeDxr2ETwEcFObdIB5IS1HwtmVI2p66ytcrCHKYIs", - "nCcLMqOK4KcuJMT0MhIjcaqNklME0MHQfDgVo0sKHQtYj8BEdUI8k7WDMNVVf2vObCNR+QJ24YzNjYlg", - "GY6cGu50BVY8Z1ghyuYR8wnPgZDVBoOBmNKM0WjG4j6RGXlUq0752OEfuxjUkWCXM1qoHK2QqlekPBn8", - "6OYqWGo5Y1StkyPawi+/YjNLPGDYkVeAxi5rLBnaojCy8LKSY5mi79HkBp6W3AOoxGb6l02EJbG2nXps", - "+CGoM5aNd92DNEwB9XkDjHBokp8l5oTtIwLVQlUs/M6y03TjXHzo3iMqz4pI943VnNllvmzKDsvuAlOz", - "Hgzh0olssfrf0GjGBSvh4ZhWxQ7Fr43sI1M0xhj7TfXyYxV0LnABAR0t6+ONaXtVpoEpJmFJWZvFwuGf", - "jYkx0uVqasRMcBaXhbSgxUoFGvFRyAvhSu2BLhobFeTVaPF0l/erkPl4IgsBvlY5Rt00rsaW190b5TDB", - "ois34u4CBB8aJtCKGOJ8U5rlHDA8VQHDnBQJmbrO0BO3xCy5EzJVTuluRMmqpBY3tdXl5nqZs6y8jshc", - "QBsF8Nu4iFiMhuxKKTtqNhxyuH1xgl5brubtvlscc5hq8POE8sQWrGiItPP/lXLaQSI3S9NDmcwlYfrM", - "DmV2KkrHDCAB4J2JYxUVMotKHtuI/80ALNOoYK1Nz/65akevbP0Svx/PcWEiJ416VJXc1OBcumNvGXVZ", - "M6X80hwIYUyjGbHeaA9D8reZPiECK+M7dlXMMmkR4DZBqdZJNGPRxyZMvXfGKgsD+lSq1Tmxbo07yEdj", - "OddjzJ9rHOfpEFXh15JPmzz5kS0C5slLPaFM5JkDoigPbeT0I1ucAh6OqYnpoTRaKER8aTgSr32aTJYe", - "3jGUi1o5E4K1kjGTd1HeQsHmNhKnoM/UKQFvWeXCsKk16aVNuDY48838a6eA9UQgTFbbmJr5rbW1h7ns", - "vu6vvOpAayy7MzisxvFcD63rTl5PSrgRmiSumtCoPP7YyZbZlAr+p6n3AKuRF5lg8ZAcQySRA6kACiA9", - "PpLzORTZHAlwX+g9sLJyEG4P8wsZCZLGNi7ujEYfmXBVRVVXHv15jeFfmU2st/MnTcrW2GAjd7T9zJVi", - "qO2cXa/fjUvDU5d416v6EFWI0p9CSEtd+Xa9pm8OWG8Nn9vPIi/Qb113iKPMh1J94GSMZxxFmAC0LLiI", - "upjxBODzwfIEhq2k2G9Cv/WTtZL/Euls9+h3vip2WYYf+h3Sm4i5B0wlFzmZ04V1IDrjoZRDjlwzKZIE", - "bk3x1wu4/GWXETP1fVk2UDw2Lhy1/PzYfgHB7IS03kSEWWQj51yDk6t+uiAbX/WA1siwaLnYN5sM3HzR", - "XJugld3a2wq7WAveEW/jW8c2v+jnADiyp2aVy8nhmdWw6BU1oYyp2UkzVvESwo+Zy8Digpxa35d+dApO", - "QXYOJ3/8fSTwoLF8f2q4I/ESk1xQ5ZC4lsyo24MDrGkWKmwR2s3XX8WwRegfc2DXLM+w+pgF7tcMYpsZ", - "b/qE3WFL62TIWlLtJxQzZeZkW4BvNYfce5pwCJHj9eOYoc76IxWd+w5pmSQ0Vei1syBoVriA7D6yAy6c", - "t7ylmawnisVkxtCWa9+GW/VLU14CGnbLUXzvXOSeva+GIqssAlSDGRVNSPC/Kl8e4GI0YREy+0jAyuPM", - "LePH9zgMKEReU6mWHT1fhacFKjMQUrR1jLJmbtPrk7cEwhbtO2WWUywjNYz5lMNtiw1WVfr3Ym6jGvf0", - "C/rNve/29/cPnn377eDg6Z67H97LxlzJse5hbMuJjTHHYjjL548tuGY1Lv/44N9+9pMZRa+SUX386B/P", - "R6P4b/Cfof7r8T/+/fgfgV/fBH/9LfjrC/j1XeDJj2u0ffL4H4//8RffoqzPcsjf89pAg748Z4BHcSvB", - "FSAkg4tphdgbQFZ4Lc5pwuNjmxF6NJO8Y0hDbfuHD5U/AXXYrobc+kO1dczN6ya0HrJ7qveYJnq3pbGK", - "SjSXMOW3kANjpwdThzo1VSQBNxzHqSuzaRVC3HhON/jvhw49oE6oEnsm48VKx6I3BgXiALT23XJ0WfMX", - "LNXmp8g3WPbYfltf+XoU6P1bfDc1Y+gL1qLjrGyTMZAfHH94C9aFN0x6jU2RWIs3rrB+rsqh9/G3IYTW", - "0EIDmKy7M55j2jsV5FsvFWSnKz+nl+MEZwxhdlEu4O+bUQd2PjstOSLlXvuSW4DejZccQwQsBuX1rTYX", - "3mpzMdZHudzAho8TecGyiCpm/l2kaeXfWO/Lvu0YhYsbYhSzCl0Y5SSnIqZZfH08sgO1vhSaGAf8Kyz4", - "Fme7y+SqLrzo24RSsC5oy21r+Lm/3odVLbH215VtZd2vvUPqup9WzzoQ5Fb1Wa8+llS4YhOF4Tij3+Nq", - "bLvianxGFfv6mfnb3JHBP7RxOja4FVyNrTKEf+gjhf0LkHfgb8NY8LcrZMnVuCi46XfyRywsBRDpgRfE", - "ZYkJPTPg/h2XeZnwPmSv9qyPR40zNmWX+hEO3XRq4b/GguUXMvs49m8nx39KwcYJV3nb2xGPs/FZIqOP", - "9TdsRrbu1zMBNzld/fzzmyOp8jcyZsm6F4g/vyH4XThyu/p2eZNX+kRilvQJG06HfTLqTdN88GzU039G", - "CS1iNng6+GqgpBAsxyqVHWtr/uIlxtb6+OH4ne3jCPogT4dfkZP2PlqTJ0MKzJ/LY8wjWxcNVX70ctDA", - "/ywgX9WMI2ZCzo0ZnsJF+EcmAo5oGs3YWK/9OGXZGN7aCtinbpfodoFGVtJAHr0/eYGFrLHzi4znbBe9", - "Q8NLuuciLfLtdvxaN7mkS1nkW+/zLbS5pFPcSLVu2mq/v9pmiVxOQV0yatMemJUlIvP2nGUZjxnCYq8p", - "NLg8WlgQEQJyDFKWDbSgqpRGll2k6WVI3iMISIpA56BL+yhjIwGVsMqytRMtgvZLQhPN/QuCIfekmhNa", - "a4pQEZf4kX3Cc3LBkwSKbqGztdG8i/WF9iufmFztbWRLdC28isFvn/s9B7a7vcpZv+HFC1fl4G3pF9db", - "re8tJT0GemaXKc+Ygv5g6cahHeyICimgEhcub3mjENycsKHwFlWLu8P29KvBllJvL+k0+tBGFLikqGw2", - "CHSJRQOAiwP35ebJ3jkTsazu46v3TteuN8fl2CoJnDV+W6I49OjWxr8RJQQEKoaMRTKL+yXMgr3xx3ec", - "7jMXwhY41ygMuLfzfr42vJwjC4DjgCKG2wESX0dBkEdl3glNLugCS42/GPUeB6nZqS7B5QwokqWEbF2x", - "IBlWq0A+FyQC28tmfA53uXbqltO6hfxZs+VVlVajo7mzA9ZWNG2p3VY7NFHor0+tBXsv1dxa3VstGAqu", - "MY8amrHRe+lHWbNvHrET/DbIgA7fFDgs0paQFrMgDVu+8/7Zg6JZppBCJlV9b2jbGMy0NbRIO5TNqr3j", - "xC1Ei7mqtNhmzBpi3q5Rmd3S8S0KqulHlLhgXHmdkdY3u4/L+drE8i4PqtYwlikTlKNlTEU+y2TKo42t", - "7mb7b1MmDl9j+4dL21/P4nYoYm0xTB7Y1ddP61hXPqw6Hfy5P/gOwNUPPj8q/zkYjj/8l/f0b+Y+fjk6", - "CRJGVC4z5gLtAAZHWOgmv+hIYesGYvgRmDDuRYwVyrDgG6NZNIPnUSaVco0tUr3TNKr7yglBxzw5GHz9", - "1LsPwDxOE02NQCQQijOCJFxcqI9SCBbl+I85UzPzs145CLga9caj3nAkqkWAmTjvPe/lTJkwqAqCxFce", - "goRZvdC6cpVDtU213Rx0PZUweVhIcbkxtUnCeb8jakElTbwTfl4gvbwUxbU/LBOqNxnl55YlO5rRbMp2", - "tGYRNt5cs3IsOwILwI7J2cIPVTxxxR1sKjmhGXuuHw3IqdmOTuEfpl4f/D3hgib4p0GnPNWf1OPr/TZd", - "AD0E2Kbo6nBfD5csh1T591TtbEH0Vnim2w9h2tB8vF6y3gqjwy6G6xRgE7jWoTV/Ret0lHVvdjUjmCeE", - "9YyuwVNThZMwBanqYBbrikezStAJttC9fyMmq5bCq6iz2wXJvY62sSxthbaWzY5Pw00BjtRpWIY6orUQ", - "fjYSlfkja2KOBBvxATo2YEuPc97pVtYduO56GXvi2vCdaU7XfggBMd5sS7fTuQbj4ty1zwLGCb8s8T1s", - "5j0GgL4w4Z7rJh26AHEPOcQGP7uUknDhnXYoUBtRH26zSPK1K1mGxt2WgLQsoasVa9Of5J1xWms6sS2z", - "mmZywhM2/jIOrlDoe9Ne04zPabYYs7kBJlq7BQSD8HInxiZ3YkOa2mQPgqF3xBPcBFwTBp205zGN13U7", - "dtqLsFfQwjPm4yQ6tfD6xXBd0MA19sKyf9uPmY+NHGEbDBlgNgFbQf8MqUaWhGFvA9/gRmM3eOF4QJTZ", - "jY4eCUBaUJp2PXjsBU8hplLPTgeOY9VdbXDy2WiIK044Dt9yFwrGZhUFU7E3UrsQRrbZDrXhTtM2cb7v", - "eEf6uepmVjvBR2xnIc80MSUv2XA6JP9Az+Tv9tmH39kfH/7+/uRF/c58FwTh/fjrFzVabLdIC8SPNS/e", - "d0cP3NeHKNIPPvweSZFTLhRQ1tvoxmk9qty9c5Um+zPOEjr6d7zNlPtLlRT8EQkx9yOtggbpYTuSMIxK", - "bVVPW58PEwV7tgBL29dKO+wKw0la5/c4obvyuugz+k5863fETX5SnLmZ3NESKL+LlSf8NbfUjT4Cw23z", - "LzdZesc321/GN2z95To0qOWQrlEpcYN3gTGbQKXWmbwguUSgB7ilc/D7nsXm3bfZx9iLKmrX4VAX8llL", - "Xcgaha8JRk+8N5Bu1UoNJjik95ehvaQ38/KXIfzxud8DAsfm5zST8xTC22MokfbD11/955uvvjp89dvh", - "Tz++PHjyy7/3j/7nu1c/mnozz3sQZaXGAHZsr31NQIci78yvftjCsqEBRHIZyw9UQktmL/YhDv0Z7KqU", - "gQUOvS/D8S9e23Ao10tbqLK8gYHa/9x/qKb25VdT20plk1/onMXknydvfzmm+YywSz0jpsqJJOwy1yRh", - "2HEmi1Tv9cDrHoII2pvgQm2EBJgCiHD9L4UCt6mA4L10RgUGhiOSmYhZpiKZsdo8eLqnoQAamtLXCCG/", - "rTGNjWRYWD+D1eIJTxXXwamWFcXLUGGOt1q2BSKcFBcGogyVupoBBJKl3yhq8l6xSQEVQtVHnhKZOK8b", - "eT0ZiVqROJokZMZVLjMIMzbWPM2YbTcefkk1+25J/brbXqCtuVXVpyygC+qqwNTMsQqg6uP9qwJd4OrZ", - "LWCe3/nvlTV3VDHvE3o+7ZM5FxgmNKeXvigqPMLYWugZgPt49VRNUFNKM2XhGfFd6PaVzIwOGgPemt9y", - "v0o8EmUVFzVFW4fkFYC/FiIficr2aufBTaUmlE8Fuhd9/eHOAivYZ+Pief3KoaKiBj+0nScPq8eQjc8S", - "ZTSiOQzqqXKpnGP7T3o+NXnWmJavBRyitkIxi0Dg7S2Hg+fxGymGA117RZjURlZctSqTrSLVhsjY6Zhx", - "3UWjkFgQziX11kYCThumBvujUY/9gWGFXIx6j73y07jxuaTXtmpU1eX4vHSBfsioKBKa8ZCSfQcHKvdC", - "pSgaHLRA9UARbIs/ZUi0G7g+tVh8JqgXOKe5L43H7w7e9Pr6P9rkOT54Af//pl3ergA7fehzkI9c6R3e", - "8FSkbain1obafw7/0/bT1J8sQ62eDEhc7j3vvX93hEXWvBaeeC18XlbnrLuFVZGs9irpdfY0krPTkn1Y", - "mY6UBUu4coU3MS78c20a1x+5z7ItJPE/WaCCH0wJGggegSKXLdWJR8KNoVbuDy0KkfOM+UVLoe3x2WJc", - "1UbLqzT6JJmwDHK2qPDl772Kd+HDGti7deXvsasHLW44t0moq3eoqfOPpzCtuh1ClXHTOGzp14e/HKJW", - "+I9+4YWpKTwSgH73fG/v4uJiyKmgQ5lN93RLA92Seoywq2XTXhX6WC//nAs0MIDnMC81XKxRtRWcfv/u", - "CN6D9l3uqWopy7mbopLLhCSXw1UqeyNE1zcV1WdBEz3Nh2eL3+s7acUtutxhZX1hcOs0yIvsTPY8qJ60", - "8MqFtmnYdt1pTILe897Bk+HTZ199DfO8aWufu0dR4RIhKjDwFFZX1krBrwBF2DzNFwiijgjeBuK7a4iV", - "t8A7rr28mda+OVnoFDlWnb4t1WeuHTl9cflipGSzk/JDqeFbXWrYrPJ2Sg17HWARxYbUYXdrlRG2WKaI", - "YbrBZsXF1HhCJzJJ5IVNQD9KZIHIqMolmDfdoaVGrwicRMNxnupDz48sSWSfXMgsif8vGBb4PyoHJyeR", - "INlfRQf7ExqzwUH0HRs8i7+OBt8++earQfTVk+jp1988PYifRmVO5vOeqcAwMP4RTe45yxSO8mC43/PC", - "u5wSGYBLBYOwKhqgdptTvVJq3dG6lqcqPc8pXSSSxkNibwj6hE+I8eYRnnvup3+evP2FSBM61lrlveQK", - "TRRUvBJ52P99hA/Rl2Mkw19x2HuRS8lbbTWXojLqmeKHAJ38v0qKUY9wNRJUs489uf/47t2xb4HWv9HM", - "XDrFGk87VLLXJKLgLc2hhXMsvGbuOvXIaDxjmX4IqPUOpLnIeMMtt5KOpYmvqrwUqboBO7L4Cg+zWp1D", - "jGASUPxMb70XMw53u4YHZzRNmaj7KGvy5M/PwMcjW0WdL4e+GYQiGTCD8OUQQ1ZUkBlFed9UmFSwcgjY", - "xSoCy5jP2k0B/OvMso8pDWQLk0GX5hhjp7byzNRSy0bikUM9iMvYpMdVUqsKaQXJmwWNrgKKd/gQMjKx", - "MqxkDfJG6yEUGW0j/vrqiDx9+vS76iiWaNCVItSuoygXihhNZC5Qz+wOZXUXznnGoAqp9cLIjGO5ETEd", - "iXJUtZmX86H511DJOYOWNnHMO3QAn+XNlyWbfahVhdcDeWm6bN3YqyDla+PNuBuNeXWzh5RrfFipyFLd", - "3P26IqvO3PbO0717Lbv4GK88NtjGK8H3y770otQDrz3B1z5fvSSLTchxxe1XFWXhFX2k/IwGWF6/Xq2/", - "Omt4At3JMkyKkbLE6UQvmeBKyQ3LLsChFld7JgM857nyayiBEWID/TdPOtiAKuzN7hax8dFZ44LHWE2i", - "rU6U8Q2a12y5KLPXVG9a13OA1BXLv1wnrlbU0swyqyn9Rfan9oNfidCQuZ6qq1O0nuaDJppTt4Mas2UD", - "V68tG8AwwuKwxAG2AfBV1wqpR1j5tHtVVt3lrJhTQW5LZdZfZP5KFiIumaBbzi5VzHLyp0bZhNwk4ZUn", - "gV+ktn8LEaPmUjkVdbDpj1JMn+cZjdjzgydPn3319TfffrdfjfF0Lz/bf/bZTUe9n1e2H3vsKR/bsjXw", - "XwPEAJVrnu0/C/mrP+gZMk6HJi6OiahyESJKn/tpdsbzjGYLbWlGHM7bJkaiWpxmNBr84/f9wXcf/vZo", - "NBriXy0ING+9wlamLuk7eqm5b+0akV5Lg4Sds4QYs4Hk9BK531kgBg5DKx08qNdfVVinH0+FDiXPpJ/a", - "o7vF0tQmrF+hyxSN1BOkN5Uil3Oa8wjqWZfnZb+kF1dLsDK3G2JZOb3bAMowBh9WAAeIhHFOL9fExjDr", - "uOxM8qK+QAYQtQpI4XSS98B9glJ3LgF46xqpdH1WS+eaHyvk7bJOlxeq1QG2rDlPbau8HjjZMZ2yNyx0", - "P+MssbSs4Yh3XB7Sui2RCjHULkRrYtxW1ZuaqmwY7HRfE5ZmlwNWvxyolNGPjKrFIGdZRicymw8wxqpE", - "t+N/VlWqF6mxXksYCl5tarO2auvnPgVabUctC+JFBzVWBScUNZ+3MNzEf6AJWp3p1Oz6K2KSWDAcKbV7", - "tJGTKnl1+jtO83FCBeBtrRvMZb+rb2/GBtIzcWbCOSnkBWFCgzAIXH1Q/5mErWAkjFsMwbkgtqas3qgV", - "RVpk0YwqBoYVt00GjpNuKJ0UBIzAaS1TFiF4dwCUNSw8WHpDiDtqwqsPYf13Iqx/ksn5GNKQUs1+3eep", - "EkAdZKmPzEWagXhABxjHW7pXDd9ZXNuwUPjc90p/X7a3vaDxdUPA5/Ry/EdBYa3bbCtcmHKrAq7xx23j", - "ic1AYxfvCkJHXsnMFhcdWKPBKRFAQ4XiMyUmoqnrCpGK8yLJeeMzrYqYKCHZCgGFjFlM7GAaRA290kF+", - "6e839NJ91AsVc3qIku8eJb/KU2OLugddDp38MsdwaZAf0ZwmchpwyLRZ2/+qd7mqznu3WHLcypoqKHhS", - "sbvx7Y3TLg8aNxKrrbu/vZNjcKc0kTc0PRmP2O2dnwrm/w1N0DLtsN4cNRSGAXAA1HJwjrE4dKzhShVs", - "By5UlWdFlBcZi61PZtuu1DfoRi3rXsC4DWzn+v5TV0WveahIqT6W4x0kvFa9cQSlqvbSTO5lNGcRzWK1", - "BwExewa75ie4z2ot+26K2XV36dYKflyjO9fOU4idG1kga4cSov/BFM035TEwE8zx45C8TVlGc83h2qSb", - "F3kB7jt2GSWF4uesDwmoIwE16s27cJNmQlloTqgBT2pwvQgVapHzM8ik97DIY0OkspdyiZxCkuXhLy86", - "Hw6a81WLQV9WSg/EAj04LdlddsaIfa86ABOuFspy/WNViybdpmN7XKxqjysCqzUJNKmWF+qvxewvnTDR", - "dcZc4STzQdeRitVTV9Zk0nPYueFOc6ibvY55lNkVpeTtr9ckJOWFDWongm0QAwfSRYnZVLar6zJB9IgJ", - "eJqMWpvTdLVqG4mabiMPqu2WqDbELV7ZJrzlNWBrkT4oxwflePuU4xuaEv3NEi35K4uKTL98DDkoaypH", - "97VNYcEZEISKaAaaEpz6XOQsO6dJSJnp97bjWgIP0QCifEz3uYSSAMZJViO1Dh2yLDrNGjemWSCg37PD", - "6k7+65O33369f/DC5Am3+H5tuy6f2E8gJl7+sKP9GBKIyytS873/mWvLxAvXzQOzEt6oPgTZpXRcN4Tj", - "ELzWgAFhwGR89Acvj9GE3S6gPLlFnPXHY3818YN+sZ5nq4v1fPjbo388H7t/PP6vv3iTY0dA0JRraIgT", - "meUgRGFNdEpVdEpUMZnwS1Do9qKBVoMclMxyIrPY4HqpiAltwQ8RXuNUN+w3Y2cGwyD0xOg38BtsZjgS", - "b4ok52nCsPHSsCdzugCfs9OEnAKU2HxOiWIpzcDbknCVD0fCgYYIafyt5vMmDao4G5Sq9xGbPid/nUg5", - "PKMZ0PfXx7UiP57DEl7w5r2c19CkN0D2QDcsjEjV31+rqH77bvgviFuunDywYKthW4xrfjQp/vxzgbBr", - "jzufRbBtLHZQwhqEu1jrQILVBLOC9UsvhrvCsOkpj4QUA1EkyeP/xmgYnJnmFyNBz8wX+u3wyWaat42P", - "KzKFFc+0jIvWKUzYJY/kNKPpjEcGy4GFJ3Oas669ycweL2S3nkdiadfJsnEmTKmtDTJZOsiyq7VHuLxb", - "0c6pgQNbR0aV7QL2q4mBgNscTHehOQF5GlgYQ3fNCepqEDN7o5bOMijng+fSkTAGmMH68RNfDvUp6KWI", - "JGhYaOeFbWbpabA5ltAEhUfBLmmUk1s4ikAcYhs4hgwwM6oH5K2zBWE8n7HMjFZmxFOGQ3KYJA47ipui", - "VHZD/G+7HeG3xtb1thczWwbVZQghKVM5MLSbI/Wwcib2XhnweSqzHMNm9EmgN+X5rDiDeEyZMoEZFbL8", - "e4+mfO/86Z6FG/kc2ncQ2nN7m89OtobdiPED69dZvxwmsDqpcvpIXIHV3anIerF0zwa3F3lwtTg03tuS", - "THgQwZvEfmHcVxmUYYxiH3k4YAduufRz1zgDWBkaQ0pdJRfet0saEQd0q/WhN6HVS5lfTun1R7zVF/oh", - "8u0ORr7dTNTY7QiJWh635gLBDCUoKXoDgRr4B4ilCVFppB5e1hYr9j/L4sRsh9fNrhiGmEpu0q1tdp2b", - "gAuqANVEJucGx2b3xca3H192pegsL8DPX6Z+Zb8L+dkam/Dtjb5pnhduJATHJ+PWh3L5xN7MdJmcnFs/", - "U4bOG52kMnFpvYkxn5cpccOWuvyrd8+mFgrRC+82diX9K3n0XvBzlimIsHmP3vmffc8RPDiRWQ6BSA6i", - "JKvBYSyF9fJd8fuDbz78DrXxf/znT29+OR68+9fgPx8+Pfnqs++NB4oDe2y93EfFIl89XZsY6WseZtaz", - "6WENLK6Us+C30GWrwV/rcfvmve7AGvfmBtTCfW9k3HdY1W3Z+zA3N2DtQ7++rb/UyjeysWXz/r2gRT6T", - "Gf+T7Tpt+7WAeHvINtUsRvEUv40E7oNwArc/uLVzuA/acrjfw9HOK3z+8lKrOZqcsNwUAt4MjNl8Rc5k", - "vAC7AA6RFkGHmV5IShdQtli57kwldcRntIW0Ib20ucVcqc77sXHjHCMJ5YBba79rG0iwi3aim8B6hsLg", - "jgZTb6pSbnuSqa1IOSRY+RqF2eCpFYLnY6i3iJoCE3tGwhz0mxPtPlh7rs343gueH+nvm7PqrPqUZQPd", - "EVaCrFSLwgrso96cioImo56Ju53wSxZXv+sTmY3EqJck81FPq65Eyo+kSLFRVyzCFZy0wCgQTRETRCRi", - "GWI2D84W/iXAkJwwqBd/KookgRLwUcKoQYu+NHXIHCn/DclUQAOj54xoRi5ENKNiinPcgKeyutS2EEYK", - "RsYB7JLN2Aaxgo1t6OGk19XeQ22h21db6Ev0K7Uz8RL8js1Ye0mDKxn+AbBiG4AV4cVWLMuNB36j0g5w", - "8VJAM7daY9kD4Hi9etcwvtfm23cg+8HTh/9K1QurhuQI03NHPXTBjnpEZnrPNKFVo56/dNto7Yt1c2c0", - "Z2NIgQo7uvVzAs9rru6uRp05/Pyqz9Y0w/O6DdJzba/KljWu2CpLVYj/0C5raWpR/V7QnG4oddVGVsqf", - "PdWPnbJY+7hY69JaI07RBOTivT63GURCh1A4o4pQknDxkcWlteHoIjRNfWl42XgDzbOMryPF4TGcYCub", - "EI6f1om1DbZrWptVm8kJTza0LaptdNC9BmM3ENQHYTaASsCrkaQpNh4OzbsF2vzL1G6qAOyxbG3GPaZZ", - "a8kdIN4ccnN9YPbQfhw4he0ZF9vCnZlVHokSMEeTeSGzj5PE1HJYh8zf7IdhSm23tn0w07mYlnBrlqI2", - "jesm0COy7zi8XelaqTd0bq54Aw2tFECapmMHCX8FdRW6eUzTUkFZsF4X+1B/qOfDrMHYTvTanGg113JA", - "2hqHOT506LNaukydIBtB3q9ooHoLjYpBZpTfV99bpnxLNN2rLP3K9bZTTONY27NrnTXhg+VTapp1HnI7", - "o+S9Q6Ohl4h4G7IM7Hy5zvqVWvhdcYnxC7wl+9CcKny8hEYzmv5yYm1DD7vOFXadNONzmi3GbG6c5QGI", - "AnyFwCutHOYtzLH54CW0GYLwUXTKxjaPY61a5tYXbLqF8u+HXkNNfntD0xRsXekl7YG3kMWmuJGJZXf6", - "8Gzh+Y8Mhnjl1gS+qnQb2pLatxwHLrOZtinB7L4A2/ouQJI9gIndSTCx8DmyC4BVKcabS/CXILxf6Kam", - "163FSVQKti2QaTKn0cOQ0Xlq4WH8sGFyaLSLuuB5NDM1QpS5NMhN5dEY7z/d8RRrkJLDnCSMKkwSx2ag", - "FCGy3rruKYAGs5qpmoNtN+ByjL1mlFKayXEG941jJrQmjCueALzUCnsD0kwO8FM9APO1Z6DVoEWPy9dt", - "T02XQVgIDfXtsuc84puIn42D6mKdzfH8oEIoUXiy0GueplBfBi6nHJz4uitryDpMU9O074E8NF34PRBH", - "XHOZH7THFveECiMEmbJ2ED3BU2MwK90808eX4crqQXA7MZGhJWQCr4Qhvz1KZBETQXN+bmt2uhI+elqs", - "TjJVc7Cm7+HxawR4USOxkAVkwkNRDTz7qr6BmsHLdWi1D63NqdA92GWoXICVhOk3f5JC6MG6CkajYn//", - "ydfEmZrHr3v9Xlm4Z3+4PzyA+LCUCZry3vPe0+E+VPNJaT4DbvLjiQAgTf84ZXkLzCZNEj+eHdFtuBSv", - "497zXsJVPjCt6C4sPnnr8bR8Zc+LVeVSYC75535juSE33hzMLKq5V9YUY1PJia0f30impxl7rvl8QE6h", - "LP2AnH5kC/xD8yf+VcZ+n5JHRps/hidlIPipbmYboAGkxAwYibVAA+DmPE3g9tPoZq5n6Q+TiY86oKc7", - "7vV7ZcG+pQHfLpMf3P8LYMOJzOaB1TApbSvXoxema2Kj5LpRpvkPLDV1rNlGmSA7j8yYsfRtGSto+weW", - "frK/b/ECbCmmeuHD5586UrIkZh+UTMcI6s/93jOkKtSZo37vexrbrRk+OVj9ST1U7tn+09UfvZLZGY9j", - "hgcJVcznNFs4wcdF1rqJ6g38d083GYBMYhAytVK/HBTaIHKhPfqsZIKnai4sEDVCIaysvGOsKhaUx4FN", - "vDBnjO9lvNjamiIdFUfC5+oGZoZR46qD7XJViIHQPWF00hfIP3aJMUVwUwb63G/uVXuf4L+v48/IWAkL", - "QS+cyEmOiXelB2NBeNzkM3zJ8VltBwMdBsG5ToWZ7nt1Pumq00z8f1NZPQsVxoTExS+DAfQXz1Z/YUtM", - "1TimuWJX0DvBw8wPLF/BC1OW3wZG2L8u/XI32arfe3bQYSg/SMFqPFhyyFX2vCLAexj4V9ZDaeNAPGve", - "EBNuf4cNhMB12mGvTQKcB+NBEHxBsOy6y+17j2bRjJ/D5h0+Jx7iC57UGDu3KTemrXulvY1deh8OB44T", - "KmywK8ZMi7OEq1k7Yx7jC10Y07T1wJh3kzEdJ+yEMdN0hWsObiCThMVEv9vmndPNbMU3t1OeStP75l7B", - "dWlyyqF+8CHADHufaJoay7fdxBFVtmgxc9K0mzbSHd5mXVTG8wUVUpreBzUE6w4r2pGbzHXGHkaGMbX3", - "yfy1krvcTYj5QFsyr1/ARdKvXoXMSZEk7hV7z0G4iJICvNkJFwxA91Xf5r5ixrHqY0lT1R8JKuIyoLWS", - "GkmUoKmayTzM3abjThzuBv4FcPlrM64Ap78u5/pecHudD4O8b2/JTkwwdEMOTHimua9u32jL9yDcGy+a", - "TJybuxpcqJzNWzZhr6MdXpN5ZF7xpiySMatfhJnLsocrMLgC6zLXMFnSlhrxvtCss0jBoEF1KZNEXugx", - "eoWen5sPf9evfvg7xldu72LtyJFz05drNhT5nh0AKzqhqbzK9UFbAXD8BU3s5rTCeihb3zOc02rPugs5", + "W2EbodUNts4IcD8U1Upue0boWMwOeB2VHWT86e+wkTQcElgusRjozbqkd9+7EUFjgb6RsJW0adqM1ZDv", + "wUujLkCkPHw77d++0i8k87+qVx8rchDMHV6+xrZ7ygu23Ckja89GXS0tBc536YdSjBurmGlJFIynNjoP", + "g2+tWOQES4ofbKTCNFIX6Ig/R3gi/r6437jTcAdBBWcPvwevAst3eKgBbGMGHxjEHiR3QVnDeq+ef9Fs", + "9DbivoQFV7rL54lkLYt7qmtURR+aSV34G8ZN55KjdGmb2VPdDJMNE0W/UnPh/Z+VT3WbZAwK+GAiv3Ls", + "DUO0oYgMXOK1vK3EDoUxsAnNkjKD0rfEvEJoE23gdDTplGZMpLQgGyn0mjw5xioLoFzBnw5ekOWzo2ff", + "HB4dHx4dXx4dvYD//9+WZnStrR0Vgt+wQtFiS56kdFtdGNs+tMwPYEn4M1aBeuK+OfR54CSl24Oewhbf", + "1/D9ZRR4wz0u7ilKweO1CldQjXvf2USnGvolxF/+OsGPeUbFfZ0V+H47zolcMKhCW28x6YtVoxWS+uz8", + "zrYbuMSDlUH6bck791/ZKCSCIyocDX2yX1Ahn1iEZ3doZ0gWp03e7E/j/TA54ZSKhGWjIzv/VjKlUQuE", + "CbCyi+qVIjTf7BPCGUL7KuX6EqdpuPY3G5ZyqtsFxs5WULTCVmxNI71pld2D0U7dNNl2NljUOoVE5E9B", + "YVUbRz3KYb8JOcwA17h50/bV8wmQlnH5uKAmVr2sZIsk1UItESGx0L5A1LNg7mGe8FjQ0sULtDaywvUf", + "OLZ0KCRQZoBDZk1N5Av++YFtfQa0OyDIOL2Seg0ctnZsc3H2stb/pF4NuCYbPkDheoOGoFL9544H2ESM", + "eO4jUO5bjayq1NohNzzIZb0vsRXoBxzVvwcdCmwFI1/Tna+sFtXiHbXJyNlqLuIHSnrOM5CP77Eir8dS", + "sLEQcuwjKrYD0TUXnxe+bPBlrTJhdPuNWsFxFERZCFYdnotYteFe2HoLEbYe66NO9Ck60UMIu7V7BD90", + "RDBiaZxpFceHf3CNXDT94IrBzshrWcxFhwSK9426gMEbOBCSMrBbuobQwfdzUW+V2nYbODHE0naPp/dj", + "RO0904bcsHEyt20H/0kn2dtrQNcJkY0mq04NW9KwuwcBQbBbW/w1QoeCqAsPSutAETsWwqHHCCT8E0zU", + "TuwpUIt61JQeNaVHTelRU3rUlB41pUdN6VFTetSUvhhNaYeCMFCgDFSldqhReHkNQAR1Dpsonja1JBVV", + "k85WSNW9H6GuebkuFWWeS6yWtY1cVmp7GbjLZ4RADL2rt7QOvRmjUgTiOHklys3OCP9A8t2FY5hvMJ5t", + "Drvf09TmkILkbhBpsR6gey5CR4zNgWd32nfKBUl5wES2PdBcmNENOTss8hngu71OXxRouOl9yzXUEFcx", + "41qb2KDvFKZH+Q6NdaKv2cZmR1vvYzXLLyB2DnbNhjAj38z37TyBTSd+QcGl2i62ZOiLMADgcIPmaPbY", + "nxnWWu+B98VtUG7Xts4MTGaeSPuWSKmPxqgWRd89i8Xe+whm47M1RuPlGshLunol1C5t1SHQ0RDfGIAL", + "/5/Orzy0R8BFWLdrBAGx3eerZ4DTfKVqNbpsqzRroKj3eedKlbbN+4UuykSXBXTVppDwfAhZweZ1MVhk", + "GZYLW2IJVojKrucL1tI/bEGYA0gEBCgd1ErTrSLQgb2m2WONAbL8wLZLEEhc4uAVS2ipmE0nBLGPFtXu", + "7bzmS1+OLKWa/sY7arcu1WMb64drYx3G1bc1F3pHXJy+rQMLDLnchM0mkYNvfV9+6HSsbLHkqhayL94N", + "EnbK0rlwWbaxauVWCgdx0v13D8Wxwf5jGRNkLKRMGTHXSsRFtErowubyRujpG/wF8lzyHOgHJo25yceW", + "f7V7OclzO3XYYufELhGuQDxw7Vpfn39N0ccKoL9IBdDfS+GdLzJIcmCzvBohet9L1RsUZBxdHETS2jTS", + "pbqNresIqWydBZQtuVs7sc8RPa6apahqNdrtwKo+UDTb007WruGT7k6u81sOVth1KHuk1l2grOrYU6Ux", + "V/l2qj+/btR51DLt3k93iQTWXln5wiLFE0NpwTsT/WH66cAIW5+snvfq5rOdAi1CvlK+1rSbyi3YPNar", + "IIWQWT/MYv+irP5EnU/nzE+FmMsLlmDXoVhHZDcqqNxaHWmt2lvfhxi4XvA9np6H/wLHDwEaP+0AtPVj", + "8/UNA6+ZudlxD4EKVDVBfQ8BsOMv3bLYjdf/c8HTZcPIH3yq6Qem5sKggEGWvzXaLRHDM/wKakYoGZ9C", + "aZ5l5JYLZRaei9ZQfi1kwdIoyQnQtW8CSv/xebydvYwB4KwhO2lY+8aPI2o7734zNbwrV9+zg0oVBd0o", + "L1hgvmbt5UJxPxAxvn2+BycYTPovqoe6d261UfBwnqF4sr2Lyc8OWe+frLXO1YunT1OZqJm/oZunmt6Z", + "/zsELn9Qt+bruyRdHB/h/4NiydpgdfJi8n/CT/N5+o8/fvznycMg8KymRJ92svPAtODFBCJksaEZ1L4L", + "L8ytcNFfsuDXXHhdXW/nIpVJ6QoVBBfk62c7RVAHtNQ0G5vwjINckVAXcbPZYJsTI5BlGXExMhA1gwnR", + "jkV60fqB++RoAydG0jWleh/6Re+YmgZ1XGk9lbvPNFu12LFRQwtY8H6htwng0DKEX5WWXlSH5SKWehMq", + "bVATgvfRZcw/JLwpM68ZRK1Cltdr55+s4T30j/bCb8d6+P1x/ZI78Iv2geoL/1XAwlYX3kL0ECBDUXlb", + "KSBNrUFY5q1LX2W8d8H/yoFpT6ixC2/zeuhdZAbAbctAN2InZ6J3Jw8Mf72+YkjwuuC9bEB5n/CtgG30", + "kUMbffoJ1PASIW/qn0gj61jvuk1db6VJYNsEoEnSHP562HVQvHaPNnRdhW3RBF1QoVay2ChS0FtfzCvo", + "HceFlo4dHuJVx2KXljwGtfTnIqy7w8zx4UrkxF4ya3SoqkBm7IZl3vVDN1WpvISKubhCZRVfVcpXoJxo", + "1/IO2rlixV0M1UPZykW8QFlB7HOH3fLIn79/QQzKAKrvsBDuNEDQa5poWXx3zP40nYtCliLl4vq7hHGz", + "ebhseUa3BqvfzSd//n4+qZZRLJEidQutZVmoQWt9/e3R0YDFYEJc71QqTf6FPDv638iGFtdchOu4CsGx", + "Xc2emeHmTkHvwY05U6C+H5hQAxHz7QBY3xg45+LmmJjXdUMz8x7tmbzEWsdQa/qJBZaz4mDgHqoR5F+w", + "MSnOhG/3u2MILz7HWsjhL1PfZ++cFecfrg8i24Ushfpy9UFT0tw5+RfShOEgVofLzrpYwbT3RiphNt8S", + "tur+iEFtbn+2cf9rWZAlbnX5giyrBn7fwct/2kL2cuaHuROJDfxf/zM6ci7e2KhYSnKpOHRhFlIcQt1a", + "oOyNAl8OcIsklJ3MtVqUIlbM8oSsyw0VQJOgIwQY2KtkFw8nVLr26ol1byvbghS90T6cN5eF4TzQMHNK", + "8JGb/7Xvz/znG/ti5pMD2KfzWljbhvVGT4mQuDKCBVGUIm22j3Xm/OrLmHvVn+Un1D3/yc/RYRouuF5v", + "mOZJdXWghHieVw24Izxi23GMFcxQ55YlvIrntOn5R9HUE19ZOWUJ39DMEHjbLKpg0LEQJQD3GPEQRLYl", + "G0YFF9erMsPgPfeFCweYT4QUbD6pV6M8Ik/gQ/Pft2uZMe8mrvUMeesm83shT+oQHkTiQqcTB8QnHJ1b", + "OggDdQUOzIaiXQv83iEytCHktZrEbuHZeGsOoDTEkVmGPBHST3swI2FlaCgOqAjNbulW+aq3iCkGTVlg", + "IEsP2k//bQhpSyqrrtE0QkkHCU0/ha9n4MV39nWLqdrFR+kjlIts81MbaOuJ7EustB9WSfX4tpa0pEn3", + "HOkBsYX8r/9vLo7Zn8h35M/fH2DAbUCL37h2AHutkEil5+J//U9yPHtGvoN/1sSKgzCCAPc0mU7c8n3R", + "Ax03N4r9T7+nb4NX7sZDUIedxMUrVwSrdnVdcWK8wnPRuLyeCqEygqSnwqk7cysQLF8QAIeUuVseQkwt", + "PSBP9DbnCUUuZXs2HCq9zbxTxp7yKpOy8LOlhnf5AvjshstSuTnx+zXNVosy9yP84rRgyq8/xTCqo9k3", + "Fd7LfGZrrgqGtVbdT//aOAPEgHM+UXXI6xUPEQWT6QSAn0wnFqjJFMlUz435a9hPcXxHs1a7xJgpN8tY", + "shcPdcCd+iku7DJdCZz+w0j/Rt928eP0ExoYOpi8w2AHSJWhvh+ifZsZOnhsI5sd0LhGOf2w7NPW0MFx", + "Se92wKDpXe/6PW6b9oU4yfi1iDdQABbjfrYFcnEYNL5nJGf4EDMuMMbW8hbqZdUWO66nD9yuebIG4Ueh", + "X/+WYTt4n9xGIaHXBXZC/fW58AGfjUQe238c+1WQJ0hwfY8jAPAAAs9E6n+eC9tByf6+q38D5lXu7L7Q", + "g+gTN8N0omr1LfedsJ4VfT+tGYYv96nTeXT09VsYMn4vAjxgYh+FoGzd/eBFGM177JNoEXifNz26+v1b", + "N/LcFpjvqH7v8rLtA3GKpt/KbFh96uD7IEzc4f/9wBrUre3ud+6fUBt23OQPfv57Ir5GPN5/alOlHlFh", + "HHLdRDVZwmNwLW9ROYHfulAGnkzhE/YiLsyQa32iSFRxwJqC+g+P0AZ5vX8miQIVNqeomRkm55fH/xZR", + "kjl0VklcYwlo2OKa9UFoVUadCuXPwEXCt8CZi1qYVXXXfKA3Cs6rMoPGKonNT7L6VD1JSZE1vQFhLePX", + "a51tK9t3kDkBUKt6UMH58cuwd4ntwhENGHD6d/SquVu7+6q3JdBxN/3MSiU1WYy2OqFHbm+p5cKKJbXz", + "jgXq/NVF00tixm2oNmpYtsUMl1reS1XdJC3oSiNTIGuqSE6Vi4JrddyBbwPWE9y+o5fxvpJ1NuKWxwal", + "H1gOZUNgWlfp1Hy6oaKkGTFKILvd8/ABk9cFU+C4spjeicILjAgKhvpDurKNdGJ9wv9LFF3hbV8wkVbd", + "NvbLlW7fxjBFVUSbp7gDMrtY0ESXNHMH2Nz7qRS6kBlS3hD2Q0sI1FoWmgkj52DPaPc8QRJHH9Ys/ux4", + "4wGMenN9u2wxw7a6XMvNFWmjVUutDaFB0qrMAhRF8DZAxbcaIgZ6nIRvcU8Cch5TJtsEJMjnrujchum1", + "TKEoAsAzFzXq0GdGWODYuMjRXqLxzGuGE1h6UVt5tyjShmSAXOLVc5FauvurIl0xkfbIJw+BbLOkb8gR", + "u65pyRbAAhpU/Ot+Mo5cA1XxkJxzRdKSzchfuV4TJTeB8UNmJTJ7rr9SYLKci6DBeUDtWzts0H2EbRDX", + "v78rVN2bva7GLhNB9FWMMkL1kZhp/SKMNG61X0/bYNBG7r7WgwHbGWlA6NvIgCsQGtfGZwiOFvTsI1x8", + "Ql/qvn7U1rAWxASXHd5P7zo2kFeiP6ZH464IrSJin9Rj1Z9CWPupTNlZujzAxqk2AyPIudD0zpVLxH6F", + "LgAdA5yoIEKSTIprVoDYhTF0tnAhS1/Y8NtrKvjfKRZfTGsbTCsTPhdKM5rOyKs7rrQNGodYZVfTcUO3", + "NgL/CvqnyRuWTucCUgBc9sfS96k04sG2KnYCGpltCr1TunwF31XCOUJbBeZaToLJsK16KjTPoWQlsURx", + "Ohfg6bAZFF5ewlksTFM3XVUjj9xw6gZd0ru4qM/EShZJY0/xsHz8co+9/LUOZ5gxj93RsFaMkBol79DY", + "K6oUfq7XstTN9WdzYSHDbo0qCK3KWWEgUFO45A6dDic0z10xKBVWUZsLyLLGy5/JJNgm2JHxZeSUp61i", + "HE38xkTk0FYfE4+RNJ5T+D/DTVi6ZzHQc2g06SYhhZ0lQpSoplBlY0QmMoJZWQmrtOINw9n6JvFbe2M+", + "bvJygMdOFCPhp5lULD2vTKPDcXLiZBx4Uq48EpwrECWjOKRUM2VtHV4m4j4iwIdPugjAYPRd4ycmIv3t", + "VoXc3EsC6qVf3JtyoPdjG/Q4zHXJ69nRs69dXczjF/D/Z0dHx/+tlsRFC41hsve2AaizuwP8LryOAv6V", + "aPdbhaOA7UTvmRSrjCf6VVGMypakiuEQI8o0+b+mHEx6FexuGTT6Ke2MQNUXH6S4fqELmrAXx8++fv7N", + "t3/445+O6m1p/MfPj/5UkZ2uZZyBufrVZf3A/8KsTEHaz/OjP8UciO8BP0p/TxVXnz+1cpD+SgRLlkIX", + "23hm0s9nFz+Rr4+//fbwuEq+ur29nXElZ7K4fsqVPITfE5wGM7Bma73JDgjN8jU9fEbsb0beStnMlaXV", + "t/IwYxrjPOADW9QCUkAyJQOWbfud3jDBIb2x9r7eXTTS7WuZTs9qGV8/nxz+t/f/eIbpXk2VFOsin6Sp", + "FLY/zFgKbobuLI6MRZuT7Wjp+tQOPO1uTOzmRgHU0i6apodWCPAVUC0In0mpCEtZRqbiA7rP7NiebPzw", + "kzpO1IycojdiPlFcXGdsPjFS/tzFX7H5pJZweg+z3W+V0hOojAvZgopt+GGJlYoQtximEKSx2yzDbXgt", + "yFn4yVwEte58DU+yxKJay2Y1UJwDY3yhb71Z3NWQqip/YqBniMkvulgH9KhOaJGqjmA7l3eh2k9wDGtw", + "7Y3DSjpv/dytsjkNym+rc2ANjvoTm1ZEqLabKIMAcuYqSaEivR95rM+xk04a9Wh8qSuc/CTPfR6+6i7S", + "kdm8XHv9QTPkqlm4YYZk0rtaO1xe1vkIHi1rCvATRF1Zvz7h/TKfnwrr9I2ug9bVisEAj8ltQtNE1wrG", + "OUO3L2gXuyaGdLpqnwbM2zDYch8nWwekV13RmJVT0ELURRI8AgMgp/jeqoveTQtQv36dUf2a7UkKzGCy", + "Ysz6g3bTAkj6WWAuXOXEHI5aJ/G4DO04RcBUTZ9x55x0rpw9wFor1xMfYFDlatiOjZKqGRD6L4BVQivQ", + "yBXLpLkIWsa6LFhQPk73EEHHyJ6d6PrMxE6fMjpeEg9fgE/w7sBLkDvbCLtvISiYajqxGZoLKy1GEtNs", + "CmejnlQwt+1BVHm7wsVe2+EoILSwA05o62V/iFts5p+SUuCraXn0u2/R6zLLGh9XQd7sviqoXdparVg5", + "LSxSXmDpdS40Eyme5lWV/hBC6mJuqP5yma11oy40KzZ7SGM8YdYLdmkmaOPZuTHN/G4/qzp3qPUHCD63", + "3kAbz1LzKY2G1InaPrTjtDZdR3y95xB1h1bn1T2PD8AuBg/20rAYO79hzSeWyBvIZXIpTC14L1qv7Bdv", + "ttCJy8aHVRGqe/dcXrY8lp1AtT4dFqtcn8zFUJhnsFgxFlbevrSV+1r1I0H1XnjlfdFTxrjS8IOWRa2t", + "RD7eGelg5cu2jhkQ59ZVb1+qBtXpfuXTTsFwl/QaiK2NEDTnbasw0hc+4c+o63l2S8zTSanoNVtcUdWd", + "oRGOh64f35uv3RSfmj/RA92uUIcdgL2PtFvqlPV7T6o9+cjiHGb8ISB5qLbxKLv/hmV3x+n7xPaXXSJ7", + "2bpMDym+P0rrj9L6SGm94AnbT0zvlHF5wvokWhj6KL8+yq8t+TUUb35/ImxI/93D7JFKXRTAfkKOH75T", + "tvGPaXFvsT5Di82DZ8QABx00qhhuKIlxBdBzNRcewkhbQa5958CKKgTdyoCKui8U0+DRXAp5u/Qt4SCs", + "D1w6K071yBrPA0QpM2vDD1/VSnIbnTk34n0VqYKpwe/oF7MgtLtGV/u2MPRcS6ihd5L+R6mg3t+et7Mx", + "y0Bz/33g5s8FFdCdDkDwRSL3kqP3CMOwt/66BoWaPbr/9pZx4uQ6oNP27uy603AxXvPMADHWeYWjqpeG", + "dwsOORJSiXwgEirwA99w3Zqhak7EE6cjKEsK/b8hSMpnwtrI5dosYZYtZihv3eha+NTPEy7yUi+w2Npk", + "OpGlDv79fmDEQqMJSyswYfdZnJdFskcXQjcM7lYeWIdV7fFFaMwN5Rm94hnX20UuM74PKUDCFsx0jhPV", + "Mx+lWLiWQt0pj9hs3tdETuSGEQujzVJKpFDlJvfFBsNSJctgjeXsQewEpw2ilnvMV0Q1ZwWUEVwYfrQA", + "VrcvFXfIO54dTWLijpUYcla45wPF/XxFMZdsEIXV9gBZRuCFbgdHs2+OMBog5Bvml38+PjqaHR1hMw+s", + "n+tyGs2v35gfoSuarePkmpZfw303BIMKcuQ62tZlH3I0NfDPxXKFpaFsStN3UDTK55E0z/54djSLtPK1", + "xz+IEH4KX/8z0psvkqVbRPxmODq7y3nBVJVZOQwNNpPxZb+7K3XqYzMJs8Z4LAjt2obshhX4KxfXVgD3", + "7HfAIbXZNti9woeyJwF/bavn2fzB1uZfu9p5mPZa5ytfsq1IFlxva/lVx61ioS8LensIlfHcgDoCyA/y", + "lhUukw3yk9b8es0K//0sTJrlQh9/i8Caa25WPDoCWO0/Y8U981A2GPOwI+JFxAWNeqqNlSD2SlXxVWgD", + "VuRJ/bK5cqdY0u9gtq8FpwXqOFuOk0LxNMw9gVrCrvU89hFynWSWfne4BzWN5GP6njRBv3UtCRMKCobL", + "omCJJgW7YQKsK4m8FhwLkGOv8JrKHsRDNkTUWk80tLNPCV9B1PJXtqM7QwH5OpNXQDt9iqUZCCwXO2Z1", + "6AUN8rC3orBvF7T4cdG6ygBJRd2nQMYdwnQusEeCOWVMLsSzz7Zw+CzFZX+d7mtNh4pLofRpvrP9mm/h", + "iTktZFAPrgGQfOyxjEZOsseVGUoY9yWQv+ZUQ04yJtTUbE7t9sHTyd3htTzsyHrxS8Fce4qDtTl258FY", + "XIw0ueDsO+Wtl20x6/9n722X28aR/tFbwVFt1STPSrKdl3nJ1taWx0lmsjOZeMfJTu2OUjJMQhKeUACH", + "IG1rUvlybuLcxLmLcyfnSv6FbgAESVCiZMl2Yldt7TgiCTSA7kaj0f3r0iO29l4Z3rFd3HC9A/Iq9+SX", + "1hHU/eyMMmZ4JPKW9vpEFdGMUEVGvXcn5LlMEprZ3JIXRSYxS2XdMOrF/Ewm4VHhs5XjCg/E+FlNCzis", + "0FD+MuqBELkh4YD+///7/zUP9NBgYBs4hNpKc1l2x5iETRndRjR0DCQwlc7XSm2CD5YHEZhmy2VCsobk", + "nV+rGwH4LZRzILbAdXYNToMAjWY0/eXE3rL4gw51iW2xwC+iPnGa8TnNFmM2NznCwZti/QqBV1p5s3p3", + "DB+8gDZDN4JwgUhzrHO2SVinlVQIAjr0Gmpyqq0enEtieyzR3oESF7dtxd+WaTwHHJ4CdEzl/hG+qnTb", + "nqPVrqxM6MeGmRa2zs8KVfV5yNQVZQmWszsTmbmDykNLTUpbF6Cs92yDe5rlnkEeMYIIsDt4NDPsxdVI", + "lBgp7vaz6ub3W1Y5zXnkcxwWScpKWm9NfXPrZF1bhM0iYLEDFUrdcdyZsmxQYC0uldeqcOn9hox6iPRl", + "7AtKJvySxdXv+kRmIzHqJcl81NMin0j5gRQpNgpX13qyfv75Nf4S05zq5SzXFFlhmskiHZwt4BBtBK0W", + "nIAdbqYUDFNuohKQSVY6bKfTjE3XjKaHpg+9L1sSrco3TJ10CdVMrC8DZnB4a0IN50woLsWSe8KPgc8q", + "pP5C5ywm/zx588sxzWeEXaYA7ikF+EbZZa5JMn4cWaR6azEeNcdxuM1odoMx/cQWyt0umMxrk7+ouMqx", + "NgdAMAh0pcPTQsQsU5HMWG0e3IWgG8pfhq4SfZ0FgZRxe1wQkmpX1pRstMWzvMWvAjmkmZyneW+lLoHW", + "1XirWDUQl6K4TXtHETZHHUs/9guaZFIkcE/7gadEJrF71KgF34fKtzOucplBRRJ8E+5vbSnL4fVsfrdj", + "IwCuHtuo9SY9ARGpSwherDm54GLKlN5dYWq/UiAiTusuHGaPe898LzOiinmf0PNpn8y56MMA5/oQUnKo", + "MpWYTbWsjNCR8E+95po9pZm5d7TvOk+sCWuLsApo2XK/Snz1ttAerRFtDb4diYrWtPPgprJWpL0Uq78M", + "3d3+BgdpDF/zN4OK+LdvUMcJFVfAUnGfr96o9FtrooassuYsrkUgNDtNaAVL5RBevSV7lRaKsaZwnK53", + "X7IcZURPyQdW3vwnVBDooB7KZ+YNwxdULX7B3uJUwsP192V7Gxn19HJsa06FtyNz1+TVvIMwRZ9iK8eG", + "xBLbFpZbS+FIIKbLwEJpWNQXTLmfcJbETn5NdUIUX0B/aXw2EheVOoaFSPicV4pw1YkyvrDyosxa/fTS", + "fRQsjnd7A7dQeJuMu1yvbK5SOrvsIhozsU4sfbc7dOu6M82bXcgvSVAo3Fy4aqia76sf7z4byEeRqhNz", + "Ox1yN4yphNriKohKoAvDeEr2VVs484vBUwKRb8FSKvcGZTAxTXk+BJrN6Dw1Z/kK4ik5NBuUuuB5NBsJ", + "GUVFpsypyCE1Qr2aWlLekBzmJGEUzlDMNAN1ZVF7rYvipNXOsd3c5ly8wm89/X1cjrHXxABMMznWOkVM", + "x11Rfi0mUZrJAX4KUJX4tQcTUxPp4/J121MQOLbVTPRu1+t61K1yu2Zv3t6ufdmulVrp/mpqd0wpWbZo", + "735+9bwxSB4vIdsvDnEFg7fRzG0zfBuIwg0DeF3ts9xqc2ZQFUONHCLW7QHunWCTkbpx1WYp/WuZlZTz", + "+SY1IivFQWKev8VmWpMUtXiZIZXFw8mhK4djll0fHdOY5qxv6goywMg0yS0QSgPrY7wq2qJNuKhmcxlK", + "atxsbS7fZMQ325ncyNZmrG0FswPi29jk1gd2A3tBo1V2moJ3CZxnNoxIrauYDVmHaWqa9nH2Dk0Xfg/E", + "EdfU0nfi/uT2niOMm8JnoBZmjnn+PdYiXpeLy0hXW8z4bFEJKgkmGCRbyfE6MokBvtMJ8wFYTDI+neVE", + "yIshec4yfq73eH2ujhJGMxfvhWG7WPtkAXt8wqcc0xZgYTKm5xjrn2RFwmplrw4gth1qItL4jUgWaHts", + "NZWq0bSpALf1KYQ41TPNlDaS+6yAiRUyJwuW+/M7JK/QU6vMTI4EvaAc7CSbcQJTDTwhM3PNRyYFXNSV", + "iYpam9exwcNz2hbMX05I3+MvH2yjwqC9VhHAmNhbj41d0npD6NhAwNuMCkVBMK46XUdFpqAm+S4nzKN3", + "02lDOjebsyrU4pp3k1KwnGYLl3Ej9MnOpsMtUbaus07KoaqzurtL6pYU9tpfkW1TC/6sbysZYxYJ/fdX", + "J2+ePDr4ph1tXT8d2O58uPWRqAaAOpx1v/3KK21Q61AYpw1r/Xl1v35c2a0fB8DWH7eArRtKbr8O8vxc", + "N6CASjkMVACDlk25Mgo3w40hp3TKugu7nskGjdDEcuKOTS+11FeehSC23v36yoZSwRsEOvByRFKaz3rB", + "fOUVzekXOrcm2OXy1vQLba2JIjG2HfpfAgFz7JzLQi3twb60cS+K/xlQKeaUxmJol8BLnhQf7LuW0JHY", + "WHHzBcyQN5R2HihZ8V8FyxaWHdaQ4ecsZ9mcC6bMdRKQbqMHy8J8uSQZyzPOzkOVlsuCgoH5qC6qbluL", + "TL8sneRcHVo/uAK1XC9PRk1MSmAREHxuebeVlV7ZtQEU7tB3mAEgMN1ddmEdZy8QI2VggfhJAs4NEszV", + "NgGPn4GuRkJvSlfboPLN/JcurPzaHJguKKZZ+eXXl0ePHz/+rjREcikTNeQsn4Atom2OvWwS6ZceElRc", + "ekH1GWeQ87m7L+FiJN69PepcE8npQNeS59EyVBFN+OAtnweBhOyoXnKWxJgrCishFmvFCfVXLF4NV+eP", + "rcQi/RvCQPAcniwI+6OgiSJTfs4E+fXl0QBGX852CbfDBYFZ/tTvTfMtkgLV273UdQhRW5MctkN6ZIaT", + "ZLfU9WhLtjxVCVNq03lKtj1PJTFXmaRAfa02QBTpD1uLL+iBB7afhyZIAuKWhuQwSUgpQ/osMhLSOEr/", + "ZpMeCbukIAlSsBIZxeC9yXlKM66q1xF2ZgiIPzHyX81+M4nnQ3zo5tJ7acDn+kCEFrQ+2vSmPJ8VZ1B3", + "TKZMwJbMZfn3Hk353vnjPZvUriculALSULWBl/DyueO9M3XOVjgti5GwGZ8O7MpXvfsH31H2ZH8yePLo", + "6beDbyYHTwffPfnuYPDtk0eTJ4/oN/HXNFrtk7WF62xPlnjyUxif0kRxr10SMaKpQVYhJlUIwolcTC6R", + "UFETMACpZZoskGyItyXbwpvsCnnGTCCPQWZxHsCGnzNmCbs5+qD3FfTdfJWwNeAd0AgKOZTvk0zuk0zu", + "ZJJJv4fXyTelY8A3gyS0qZnmoaXv8nlL/V0ZSHuajFlSvZzgk/iVKVO/a40NCD/CKCErE7Agf+gm2w+t", + "LQCN+jHJ5EX3q+rGKORF6HC7tVyIE79mrx4kR9blEtMUtlRd90VZWbfRSejU3WmBTamtdcyLTF7YAIps", + "zaVeT8+sQu+cpwXASekOzdXHA1TY/9//Q5zaeDgkvxRJgkg1NgTOwNC468CRcJAwYIhHcn5m/cRyUppO", + "wIQrPYzXF2Dqjd2kfuXBXOPn8DuZMwXzg7h53rzp+QC0cnaZJpQLrEe/KPuIIKtHz9kZFG1TMjk3UOA7", + "yf3CIlC2IS+FRe/bJrFCK/Vzlg3JqckjNsA9Ntd4zONTPCJp7Zqds7i2kA3x2F0pbzjVnRXRB5YvH8zu", + "ynGvRQLI0bZEtZoRXqYSeUXCGyoMCeijyqhESZZFviu8tkTd1ezEdbczF4ksCTU2RMN09HFC6xc6K/BS", + "l88sqGk8c5+ArLym6aucNcFcnEdBk5mmycI6LjwxMpzgXONzeulTApccFYPV+9aOIzDNG50yAjWHn1cI", + "hIHYebbOfq78ma4S292Ha/ji9vrm7dH/RnzyDjvBMD6w22YICtfmlX8pszMex0zsuLi/62db1f0fh6v7", + "V/pZq7z/47by/j+wvBLKd0wzOleer39dNGciTfqN1uVTLKhaonuZcKpt1HJHxeddTby4pEucqksjDptV", + "ZjakYZ3uJ2UhmiF5AwjWcCVbZMLDxE1llk9kwqXx9o6EPhqfskuucvV3WIhT70NIyyiEjUBkcTnlwRvJ", + "H6RgO5YO3cWWBONgPywYtot1ZOJgv1UmtMEjNKlGYx1GkQWdWuNMBB8R7NRm1mCQfatZMKNqTF1nbeWx", + "PeAcZOE8WZAZVQQ/dSEhppeRGIlTfSg5RZQozD8BqxhdUuhYwKIbJqoT4pnsOQjzufW3xmYbicoXsAtn", + "bG6OCJbhyKnhTldFyHOGFaJsHoHN0A6E1E0YDMSUZoxGMxb3iczIg1oJ1ocO5NvFoI4Eu5zRQuV4Cql6", + "RUrL4Ec3V8F64hmjap18kBZ++RWbWeIBw468Kkt2WWPJ8CwKIwsvKzmWKfoeTQLsack9AL1tpn/ZRFgS", + "a9upx4bvgzpj2XjXNaRhCqjPG3AIhyb5WWIsbB/2qhaqYjGmllnTDbv40L1HVJ4Vke4bS5azy3zZlB2W", + "3QWmZj2szaUT2XLqf02jGResxEBkWhU7qMo2so9MZSRz2G+qlx+ryIqBCwjoaFkfr03bqzINTMUUS8ra", + "LBYO/2xMjJEuVzgmZoKzuKwWBy1WyiyJD0JeCFdPEnTR2KggrxCRp7u8X4XMxxNZCPC1yjHqpnE1trzu", + "3iiHCSe6ciPuLkDwoWECrYghzjelWc4BqFYVMMxJkZCp6ww9cUuOJV+ETJVTuhtRsiqpxU1tdbm5XuYs", + "K68jMhfQRgHhOS4iFuNBdqWUHTUbDjncPjtBry1X83bfLY4xphr8PKE8sVVZGiLt/H+lnHaQyM3S9FAm", + "c0mYttmhllRF6ZgBJIAuNXGsokLHopLHNuJ/MwDLNCpYUNY7/1y1o5e2SI/fj+e4MJGTRj2qSgJ2cC6d", + "2VtGXdaOUn79GcTpptGMWG+0B5T620xbiMDK+I5dFbNMWgS4TVCqdRLNWPShWYvBs7HK6pc+lWp14rdb", + "4w7y0VjO9Rjz5xrHeTpEVfi15NMmT35gi8Dx5IWeUMysNVl1pdFGTj+wxSmAPpnCrx4UqcX7xJeGI/HK", + "p8lk6eEdQ7moFZsQTisZM3kX5S0UbG4jcQr6TJ0S8JZVLgybWpNeWlQBU0yhCTLgFLCeCMSCaxtTM7+1", + "tvYwl93X/aVXAmuNZXcHDqtxPNdD67qTV5MSU4cmiSuZNSrNHzvZMptSwf80RU1gNfIiEywekmOIJHJI", + "LEABYEBEcj6HSrIjAe4LvQdWVg7C7WF+ISNB0tjGxZ3R6AMTrnSu6sqjP68x/CuzifV2/qRJ2RobbOSO", + "tp+5eiO1nbPr9btxaXjqEu96VR+iClH6UwhpqSvfrtf0zQHrreFTuy3yHP3WdYc4ynwo1QcsY7RxFGEC", + "IOHgIupixhOoEQEnT2DYSor9JvRbP1kr+S+QznaPfuerYpdl+L7fIb2JmHvAVHKRkzldWAeiOzyUcsiR", + "ayZFksCtKf56AZe/7DJipog1ywaKx8aFo5bbj+0XEMxOSOtNRJhFNnLONTi56qcLsvFVDbRGhkXLxb7Z", + "ZODmi+b6CFrZrb2tsMtpwTPxNr51bPOLfgoggHtqVrmcHJ5ZDYteURPKmJqdNGMVLyH8mLkMLC7IqfV9", + "6Uen4BRk52D54+8jgYbG8v2p4Y7ES0xyQZWDm1syo24PDrCmWajwidBuvv4qhk+EvpkDu2Zpw2ozC9yv", + "GcQ2M970CTtjS+tkyFpS7RaKmTJj2RbgW0XMFJpwCJHjdXPMUGf9kYrOfYe0TBKaKvTaWaQ/K1xAdh/Z", + "ARfOW97ymKwnisVkxvAs174Nt+qXprwENOyWo/jeusg9e18NlYRZBKgGMyqauPdfKV8e4GI0YREy+0jA", + "yuPMLePHdzgMqLZfU6mWHT1fhacFKjMQUrR1IL5mbtOrkzcEwhbtO2WWUywjNYz5lMNtiw1WVfr3Ym6j", + "Gvf0C/rNve/29/cPnnz77eDg8Z67H97LxlzJse5hbGvmjTHHYjjL5w8tgmw1Lv/44D9+9pMZRa+SUX38", + "4B/PRqP4r/Cfof7r4T/+8/AfgV9fB3/9Lfjrc/j1beDJj2u0ffLwHw//8Rf/RFmf5ZC/55XBv31xzgCP", + "4laCK0BIBhfTCrE3gKzwSpzThMfHNiP0aCZ5x5CG2vYPHyp/AurYdA259Ydqi/Wb101oPWT3VO8xTfRu", + "S2MVlWguYcpvIQfGTg+mDnVqqkgCbjiOU1dm0yqEuPGcbvDf9x16QJ1QJfZMxouVjkVvDArEAWjtu+Xo", + "subPWaqPnyLfYNlj+2195etRoHdv8d3UjKEvWIuOs7JNxkB+cPzhLVgX3jDpNTZFYi3euML6uVKe3sff", + "hgD2QgsNiMnuzniOae9UkG+9VJCdrvycXo4TnDHEkka5gL9vRh3Y+ey05AhyeO1LbrEVN15yDBGwQKvX", + "t9pceKvNxVibcrnBxh8n8oJlEVXM/LtI08q/saidfdsxChc3xChmFbowyklORUyz+Pp4ZAdqfSn+Ng74", + "V1jwLc52l8lVXXjRPxNKwbpAiret4af+eh9WtcTaX1e2lXW/9ozUdT+t2joQ5Fb1Wa82SypcsYnCcJzR", + "73E1tl1xNT6jin39xPxt7sjgH/pwOja4FVyNrTKEf2iTwv4FyDvwt2Es+NtVa+VqXBTc9Dv5IxaWAoj0", + "wAviso6Knhlw/47LvEx4H7JXe9bHo8YZm7JL/QiHbjq18F9jwfILmX0Y+7eT4z+lYOOEq7zt7YjH2fgs", + "kdGH+hs2I1v36x0BN7Gufv759ZFU+WsZs2TdC8SfXxP8Lhy5XX27vMkrfSIxS/qEDafDPhn1pmk+eDLq", + "6T+jhBYxGzwePB0oKQTLsRRrxwKyv3iJsbU+fjh+a/s4gj7I4+FTctLeR2vyZEiB+XN5jHlk66Khyg9e", + "Dhr4nwXkq5pxxEzIuTmGp3AR/oGJgCOaRjM21ms/Tlk2hre2Avap2yW6XaCRlTSQB+9OnmO1duz8IuM5", + "20Xv0PCS7rlIi3y7Hb/STS7pUhb51vt8A20u6RQ3Uq2bttrvr7ZZIpdTUJeM2rQHZmWJyLw5Z1nGY4aw", + "2GsKDS6PFhZEhIAcg5RlAy2oKqWRZRdpehmSdwgCkiKaP+jSPsrYSEC5t7I280SLoP2S0ERz/4JgyD2p", + "5oTWmiJUxCV+ZJ/w3OKMm4ztuNG8i/WF9iufmFztbWRLdK0ujMFvn/o9B7a7vfJwv+HFC1fl4G19I9db", + "re8tJT0GemaXKc+Ygv5g6cahHeyICimg3Bwub3mjENycsKHwFlWLu8P29KvBllJvL+k0+tBGFLikqGw2", + "CHSJlTGAiwP35ebJ3jkTsazu46v3TteuN8fl2CoJnDV+W6I49OjWxr8RJQQEKoaMRTKL+yXMgr3xx3ec", + "7jMXwhY41ygMuLfzfr42vJwjC4DjgCKG2wESX0dBkAdl3gnFIhKj3ruT56PewyA1O9UluJwBRbKUkK0r", + "FiTDahXI54JEYHvZjM/hLtdO3XJat5A/a7a8qtJqdDR354C1FU1barfVDk0U+utTa8HeSzW3VvdWC4aC", + "a8yjhmZs9F76Udbsm0fsBL8NMqDDNwUOi/RJSItZkIYt33n/7EHRLFNIoSNVfW9o2xjMtDW0SDuUzaq9", + "48QtRMtxVWmxzZg9iHm7RmV2S8e3KKimH1HignHldUZa/9h9XM7XJifv0lC1B2OZMkE5noypyGeZTHm0", + "8am72f6blInDV9j+4dL21ztxOxSxthgmD+zq68d1rCsfVp0O/twffAfg6gefHpT/HAzH7//He/pXcx+/", + "HJ0ECSMqlxlzgXYAgyMsdJNfdKSwxTEx/AiOMO5FjBXKsDITo1k0g+dRJpVyjS1SvdM0SljLCUHHPDkY", + "fP3Yuw/APE4TTY1AJBCKM4IkXFyoD1IIFuX4jzlTM/OzXjkIuBr1xqPecCSqla6ZOO896+VMmTCoCoLE", + "Uw9BwqxeaF25yqGyltpuDrqeSpi8sqBV+2Fqk4TzfkfUgkqaeCf8vEB6eSmKa39YJlRvMspPLUt2NKPZ", + "lO1ozSJsvLlm5Vh2BBaAHZOzhR+qeOKKO9hUckIz9kw/GpBTsx2dwj9MUUr4e8IFTfBPg055qj+px9f7", + "bboAegiwTdHV4b4eLlkOqfLvqdrZguit8Ey3H8K0ofl4vWS9FYcOuxiuU4BN4FqH1vwVrdNR1r3Z1Yxg", + "nhDWM7oGT00VTsIUpKqDWawrHs0qQSfYQvf+jZisWgqvos5uFyT3OtrGsrQV2lo2Oz4NNwU4UqdhGeqI", + "1kL42UhU5o+siTkSbMQH6NiALT3OeatbWXfguutl7Ilrw3emOV37IQTEeLMt3U7nGoyLc9c+Cxgn/KLE", + "97CZ9xgA+tyEe66bdOgCxD3kEBv87FJKwoV32qFAbUR9uM0iydeuZBkad1sC0rKErlasTX+Sd8ZprenE", + "tpZwmskJT9j48zBcoZr9pr2mGZ/TbDFmcwNMtHYLCAbh5U6MTe7EhjS1yR4EQ++IJ7gJuCYMOmnPYxqv", + "63bstBdhr6CFZ8zHSXRq4dXz4bqggWvshWX/th8zHxs5wjYYMsBsAraC/hlSjSwJw94GvsGNxm7wwtFA", + "lNmNjh4JQFpQmnY9eOxlaOpTs50PHMequ9rA8tloiCssHIdvuQsFY7OKgqnYG6ldCCPbbIfacKdpmzjf", + "d7wj/Vx1M6ud4CO2s5B3NDElL9lwOiT/QM/k7/bZ+9/ZH+///u7kef3OfBcE4f34q+c1Wmy3SAvEjzUv", + "3ndHD9zXhyjSD97/HkmRUy4UUNbb6MZpParcvXOVJvszzhI6+ne8zZT7S5UU/BEJMfcjrYIG6WE7kjCM", + "Sm1VT1ufDxMFe7aAk7avlXbYFYaTtM7vcUJ35XXRNvpOfOtfiJv8pDhzM7mjJVB+Fyst/DW31I0+goPb", + "5l9usvSOb7a/jK/Z+st1aFDLIV2jUuIG7wJjNoFKrTN5QXKJQA9wS+fg970Tm3ffZh9jL6qoXYdDXcgn", + "LXUhaxS+Ihg98c5AulUrNZjgkN5fhvaS3szLX4bwx6d+Dwgcm5/TTM5TCG+PoUTaD18//e83T58evvzt", + "8KcfXxw8+uU/+0f/+u7lj6bezLMeRFmpMYAd22tfE9ChyFvzqx+2sGxoAJFcxvIDldCS2Yt9iEN/Brsq", + "ZWCBQ+/LcPyL1zYY5XppC1WWNzBQ+5/699XUPv9qalupbPILnbOY/PPkzS/HNJ8RdqlnxFQ5kYRd5pok", + "DDvOZJHqvR543UMQwfMmuFAbIQGmACJc/0uhwG0qIHgvnVGBgeGIZCZilqlIZqw2D57uaSiAhqb0NULI", + "b2uOxkYyLKyfwWrxhKeK6+BUy4riZagwx1st2wIRTooLA1GGSl3NAALJ0m8UNXmn2KSACqHqA0+JTJzX", + "jbyajEStSBxNEjLjKpcZhBmb0zzNmG03Hn5ONftuSf26216grblV1acsoAvqqsDUzLEKoOrj/UqBLnD1", + "7BYwz2/998qaO6qY9wk9n/bJnAsME5rTS18UFZowthZ6BuA+Xj1VE9SU0kxZeEZ8F7p9KTOjg8aAt+a3", + "3K8Sj0RZxUVN0dYheQngr4XIR6Kyvdp5cFOpCeVTge5FX384W2AF+2xcPK9fMSoqavB9mz15WDVDNrYl", + "ymhEYwzqqXKpnGP7T3o+NXnWmJavBRyitkIxi0Dg7S2Hg/b4jRTDga69Ikxqo1NctSqTrSLVhsjYycy4", + "7qJRSCwI55J6ayMB1oapwf5g1GN/YFghF6PeQ6/8NG58Lum1rRpVdTk+LV2gHzIqioRmPKRk34JB5V6o", + "FEUDQwtUDxTBtvhThkS7gWurxeIzQb3AOc19aTx+e/C619f/0Uee44Pn8P+v2+XtCrDThz4H+ciVnvGG", + "VpE+Qz22Z6j9Z/A/fX6a+pNlqNWTAYnLvWe9d2+PsMia18Ijr4VPy+qcdT9hVSSrvUp6nT2N5Oy0ZB9W", + "piNlwRKuXOFNjAv/VJvG9Ufus2wLSfxPFqjgB1OCBwSPQJHLlurEI+HGUCv3hycKkfOM+UVLoe3x2WJc", + "1UbLqzT6JJmwDHK2qPDl772Kd+H9Gti7deXvsasHLW44t0moq3eoqfPNU5hW3Q6hyrhpHLb0q8NfDlEr", + "/Fe/8NzUFB4JQL97trd3cXEx5FTQocyme7qlgW5JPUTY1bJprwp9rJd/zgUeMIDnMC81XKxRtRWcfvf2", + "CN6D9l3uqWopy7mbopLLhCSXw1UqeyNE19cV1WdBEz3Nh7bF7/WdtOIWXe6wsr4wuHUa5EV2JnseVE9a", + "eOVC2zRsu+40R4Les97Bo+HjJ0+/hnnetLVP3aOocIkQFRh4Cqsra6XgV4AibJ7mCwRRRwRvA/HdNcTK", + "W+Ad117eTGvfnCx0ihyrTt+W6jPXTE5fXD4bKdnMUr4vNXyrSw2bVd5OqWGvAyyi2JA67G6tMsIWyxQx", + "TDfYrLiYGk/oRCaJvLAJ6EeJLBAZVbkE86Y7tNToFYGTeHCcp9ro+ZElieyTC5kl8f8FwwL/R8VwchIJ", + "kv00Otif0JgNDqLv2OBJ/HU0+PbRN08H0dNH0eOvv3l8ED+OypzMZz1TgWFg/COa3HOWKRzlwXC/54V3", + "OSUyAJcKBmFVNEDtNqd6pdS6o3UtT1V6nlO6SCSNh8TeEPQJnxDjzSM899xP/zx58wuRJnSstcp7yRWa", + "KKh4JfKw//sIH6Ivx0iGv+Kw9yKXkjf61FyKyqhnih8CdPL/KilGPcLVSFDNPtZy//Ht22P/BFr/RjNz", + "6RRrPO1QyV6TiIK3NIcW7Fh4zdx16pHReMYy/RBQ6x1Ic5HxhltuJR1LE19VeSlSdQN2ZPEVHma1OocY", + "wSSg+Jneei9mHO52DQ/OaJoyUfdR1uTJn5+Bj0e2ijpfDv1jEIpk4BiEL4cYsqKCzCjK+6bCpIKVQ8Au", + "VhFYxnzWbgrgX2eWfUxpIFuYDLo0Zoyd2sozU0stG4kHDvUgLmOTHlZJrSqkFSRvFjS6Cije4UPIyMTK", + "sJI1yGuth1Bk9Bnx15dH5PHjx99VR7FEg64UoXYdRblQxGgic4F6Zncoq7twzjMGVUitF0ZmHMuNiOlI", + "lKOqzbycD82/hkrOGbS0iWPeoQP4LG++LNnsfa0qvB7IC9Nl68ZeBSlfG2/G3WjMq5s9pFzjw0pFlurm", + "7tcVWWVz2ztP9+617OJjvPLYYBuvBN8v+9KLUg+89ghf+3T1kiw2IccVt19VlIVX9JHyMxpgef16tf7q", + "rOEJdJZlmBQjZYnTiV4ywZWSG5ZdgEMtrvZMBnjOc+XXUIJDiA303zzpYAOqsDe7W8TGR2cPFzzGahJt", + "daKMb9C8ZstFmb2metO6ngOkrlj+7TpxtaKWZpZZTekvsj+17/1KhIbM9VRdnaL1NB800Zy6HdSYLRu4", + "em3ZAIYRFoclDrANgK+6Vkg9wsqn3auy6i5nxZwKclsqs/4i85eyEHHJBN1ydqlilpM/Nsom5CYJr7QE", + "fpH6/FuIGDWXyqmog01/kGL6LM9oxJ4dPHr85OnX33z73X41xtO9/GT/ySc3HfV+Xtp+rNlTPrZla+C/", + "BogBKtc82X8S8le/1zNknA5NXBwTUeUiRJS2+2l2xvOMZgt90ow42NsmRqJanGY0Gvzj9/3Bd+//+mA0", + "GuJfLQg0b7zCVqYu6Vt6qblv7RqRXkuDhJ2zhJhjA8npJXK/O4EYOAytdNBQr7+qsE4/WoUOJc+kn1rT", + "3WJp6iOsX6HLFI3UE6Q3lSKXc5rzCOpZl/ayX9KLqyVYmdsNsaxY7zaAMozBhxXAASJhnNPLNbExzDou", + "s0me1xfIAKJWASmcTvIeuE9Q6s4lAG9dI5Wuz2rpXPNjhbxd1unyQrU6wJY156ltldcDJzumU/aahe5n", + "3EksLWs44h2Xh7RuS6RCDLUL0ZoYt1X1pqYqGwY73deE5bHLAatfDlTK6AdG1WKQsyyjE5nNBxhjVaLb", + "8T+rKtWL1FivJQwFrza1WVu19XOfAq22o5YF8aKDGquCE4qaz1sYbuI/8AhanenU7PorYpJYMBwptXu0", + "kZMqeXX6O07zcUIF4G2tG8xlv6tvb+YMpGfizIRzUsgLwoQGYRC4+qD+MwlbwUgYtxiCc0FsTVm9USuK", + "tMiiGVUMDlbcNhkwJ91QOikIGIHTWqYsQvDuAChrnPBg6Q0hztSEV+/D+r+IsP5JJudjSENKNft1n6dK", + "AHWQpT4wF2kG4gEdYBxv6V41fGdxbcNC4XPfS/192d72gsbXDQGf08vxHwWFtW47W+HClFsVcI0/bhtP", + "bAYau3hXEDryUma2uOjAHhqcEgE0VCg+U2IimrquEKk4L5KcNz7TqoiJEpKtEFDImMXEDqZB1NArHeSX", + "/n5NL91HvVAxp/so+e5R8qs8Nbaoe9Dl0MkvcwyXBvkRzWkipwGHTNtp+9/1LlfVee8WS45bWVMFBS0V", + "uxvf3jjt0tC4kVht3f3tnRyDO6WJvKHpyXjEbu/8VDD/b2iClmmH9eaooTAMgAOgloNzjMUhs4YrVbAd", + "uFBVnhVRXmQstj6ZbbtSX6Mbtax7AeM2sJ3r+09dFb2mUZFSbZbjHSS8Vr1xBKWq9tJM7mU0ZxHNYrUH", + "ATF7BrvmJ7jPai37borZdXfp1gp+XKM7185TiJ0bWSBrhxKi/8EUzTflMTATzPHjkLxJWUZzzeH6SDcv", + "8gLcd+wySgrFz1kfElBHAmrUm3fhJs2EstCcUAOe1OB6ESrUIudnkEnvYZHHhkhlL+USOYUky8Nfnnc2", + "DprzVYtBX1ZKD8QCPTgt2V12xoh9rzoAE64WynL9Y1WLJt2mY3tcrGqPKwKrNQk0qZYX6q/F7C+dMNF1", + "xlzhJPNB15GK1VNX1mTSc9i54U5zqJu9jnmU2RWl5M2v1yQk5YUNaieCbRADB9JFidlUtqvrMkH0iAl4", + "moxam9N0tWobiZpuI/eq7ZaoNsQtXtkmvOU1YGuR3ivHe+V4+5Tja5oS/c0SLfkri4pMv3wMOShrKkf3", + "tU1hwRkQhIpoBpoSnPpc5Cw7p0lImen3tuNaAg/RAKJ8TPe5hJIAxklWI7UOHbIsOs0ebkyzQEC/Z4fV", + "nfxXJ2++/Xr/4LnJE27x/dp2XT6xn0BMvPxhR/sxJBCXV6Tme/8z15aJF64fD8xKeKN6H2SX0nHdEI5D", + "8FoDBoQBk/HRH7w8RhN2u4Dy5BZx1h+P/dXED/rFep6sLtbz/q8P/vFs7P7x8H/+4k2OHQHBo1xDQ5zI", + "LAchCmuiU6qiU6KKyYRfgkK3Fw20GuSgZJYTmcUG10tFTOgT/BDhNU51w34zdmYwDEJPjH4Dv8FmhiPx", + "ukhyniYMGy8P9mROF+BzdpqQU4ASm88pUSylGXhbEq7y4Ug40BAhjb/VfN6kQRVng1L1PmDTZ+SriZTD", + "M5oBfV89rBX58RyW8II37+W8hia9AbIHumFhRKr+/lpF9dt3w39D3HLF8sCCrYZtMa75waT4888Fwq49", + "7GyLYNtY7KCENQh3sZZBgtUEs4L1Sy+Gu8Kw6SkPhBQDUSTJw79hNAzOTPOLkaBn5gv9dtiymeZt4+OK", + "TGHFMy3jonUKE3bJIznNaDrjkcFyYOHJnOasa28ys+aF7NbzSCztOlk2zoQptbVBJksHWXa19giXdyva", + "OTVgsHVkVNkuYL+aGAi4zcF0F5oTkKeBhTF015ygrgYxszdq6SyDcj5ol46EOYAZrB8/8eVQW0EvRCRB", + "w0I7z20zS63B5lhCExQeBbukUU5u4SgCcYht4BgywMyoHpC3zhaE8XzGMjNamRFPGQ7JYZI47ChuilLZ", + "DfFvdjvCb81Z19tezGwZVJchhKRM5cDQbkzqYcUm9l4Z8HkqsxzDZrQl0JvyfFacQTymTJnAjApZ/r1H", + "U753/njPwo18Cu07CO25vc1nJ1vDbsT4nvXrrF8OE1idVDl9JK7A6s4qsl4s3bPB7UUeXC0Ojfe2JBMe", + "RPAmsV8Y91UGZZhDsY88HDgHbrn0c9c4A1gZGkNKXSUX3j+XNCIO6FbrQ29Cq5cyv5zSq0S8rR/tVl/k", + "+6i3zz/qraWmVT3F+rojyG4kPGrlXCyPaXNBYoYylCS9uUB9/APE2YSINVIPPWuLI/vXshgy2+F1szOG", + "KKaSm1Rsm3nnJuCCKkA8kcm5wbgJsERGczaGS/jwdOrnBJ7XJrSr17Wx1f1Kc3ZEs3hZQNevrtdeGFks", + "4aIlecs+ra8/DMYWc0X3pIEQxEqyBpM556KQRTWV/vfa3tl7tP/oaQAtx9uyyleqoDkl7x6AlRtu9tHq", + "Zh+3NLu/pNnHraS833Qx35rZPmHTOSTJtq+pfTWworc+ADIUPtgeN2gCC234oBeB6uuKfmWZPLauCGXI", + "QdxYhdsbNtY0dG8kdqxdCa2N32U1k4248Vr2VGPN8p5MWKTZw397rCdwzNsU76vnTuNW+oBpxzMixFeX", + "BJ2xRIqphXTptDxoKiyJNDOEN0nQ9AUk2XHu2sVly20haBC7Yfomr9smGgxR0tFfNfud+KWu59ZlmwCb", + "lEnr98e06zimbWI2umwH9GFYBNZ+JU3PWYz77YK0xIKs3xFWNgVH9CouvfWR1T6xN7MJmBTZWz9Ths4b", + "naTSP7HexJjPywz15pR0q0MVStnoQu8G0WfXTjO829BC+lfy4J3g5yxTEKT7Di/4f/Yvn+DBicxyiGV2", + "KGdZDVFrKTKof5u/P/jm/e/7g+8OBz/+86fXvxwP3v578N/3Hx89/eRf6APFgaN4vWJYxanfwfDYwM+/", + "pg9kvWsBWAMLTekuAbbQZeudQa3H7d8Q6A7s/YAJorIVQza6H+hiTm7pygDm5gYuDKBf/7pg6UWBkY0t", + "3xC8E7TIZzLjf7JdI7+8EpCyB4AVmsUoOv+2gQFzEMaA8Qe3NgzMQRsMzDs4gyO6xg8ZFfmLS63maHLC", + "8jxh2m7frJ6D+YqcyXgBFiGc9i0IHzO9kJQudB9Eue4Iko4QzwhQMRKAUNHcYsr5W+vYhMM9NjdBx0hC", + "OeATbDV8phLsop3oJjavoTC4o8HUm8LW255kaotaD8kRgBiiMBtI1kLwfAwlm1FTYG7wSBiPTHOi3Qdr", + "z7UZ3zvB8yP9fXNW3eVAyrKB7giLSVcKTkJhMjLqzakoaDLqGUfChF+iY7L8rk9kNhKjXpLMRz2tuhIp", + "P5AixUZdvSlXs9piq0FAZkwQ1JBlWPZhcLbw4wiG5ITlus1TUSTJqf4rShg1BScuTSlTR8rfIB8baGD0", + "nBHNyIWIZlRMcY4bCJdWl9oWwsUGkHEA/mwztsFyA8Z755Vaqau9+/KEt6884edY066diZdAgG3G2ksa", + "XMnw95hX28C8Ci+2YlluLvE3qg4FsRsFNHOrNZY1AF0B0TXiG16Zb9+C7AetD/+VqtdNDckRInyMenhT", + "O+oRmek900Rnj3r+0m2jtc+2xObOL3Ab/vlO17R1yC+8Equy1MqLLiNraWqBgZ/TnG4oddVGVsqfterH", + "TlmsbS7WurSnEadoAnLxTtttBtTYgRzPqCKUJFx8YHF52nB0EZqmvjS8aLyBx7OMryPF4TGcYCubEI6f", + "1om1DbZrWgvMkckJTzY8W1Tb6KB7DUx/IC8AInXh4o1Xk1FSbDwc3X8LtPnnqd1UAfCl2dqMe0yz1qp9", + "QLyN99AGswcY6G58bM+42BYx1azySJSYe5rMC5l9mCSmHNQ6ZP5mPwxTaru17cMxnYtpidhqKWrTuG4C", + "PSL7jsPbla6VekPn5oo30NBKAaRpOnZVZa6grkI3jGlaKiiL9++uu+sP9XyYNRjbiV6bE63mWh7xWeMw", + "x4cOwF5Llyk1aJPQ+hUNVG+hUXTQjPL76nvLlG8JyH+VpV+53naKaRzr8+xatiZ8sHxKTbPOQ25nlLxz", + "gHb0EkHzQycDO1+us37PFNNYrFPaAL/Am733zanCx0toNKPpLyfWNnS/61xh10kzPqfZYszmxlkeQDnC", + "Vwi80sph3sIcmw9eQJshFEBFp2xsU0H5OiHl1hdsun2nWzr0Gmry22uapnDWlV7eP3gLWWzqI5p0OKcP", + "zxae/8iUIancmsBXlW5DW1L7luPw6TbTNiUe7mdwtv4SUE3v8Ui/SDzSsB3ZBQOzFOPNJfhzEN7PdFPT", + "69biJCoF29bYNuAr6GHI6DwNxLsOyaHRLuqC59HMlBlT5tIgN8XLY7z/dOYpBtGRw5wkjCrEmcFmoJox", + "st667ilAF7WaqQrjYjfgcoyBwNU0k+MM7hvHTGhNGFc8AXipFfYGpJkc4Kd6AOZr74BWQyc/Ll+3PTVd", + "BmEhNNS3y57ziG8ifjYOqsvpbI72gwoBTaJlodc8TaFEHVxOuYok666sIeswTU3Tvgfy0HTh90Accc1l", + "vtceW9wTKowQZMqaIXqCVmMQ2MY80+bLcGUBQridmMjQEjKBV8IAkRMlsoiJoDk/t2W/XRVAPS1WJ5nC", + "e5gldHj8CjHi1EgsZAFgOlCXC21f1TdodXi5Dq32obU5FboHuwyVC7CSMP3mT1IIPVhXBHFU7O8/+pq4", + "o+bxq16/V9b+2x/uDw8gPixlgqa896z3eLgPBQFTms+Am/x4IsBY1T9OWd6C1E2TxE97Q4A8LsWruPes", + "l3CVD0wrugtb4qTVPC1f2fPia7kUCEfzqd9YboDXMYaZy8sqK6NjPC05KdIU87PqeDw0Y880nw/IKY9P", + "4b8f2AL/0PyJf5VJOqfkgdHmD+FJmbFzqpvZBu4QKWGHRmIt3CG4OU8TuP00upnrWfrDgPmgDujpjnv9", + "Xlnzd2nqjQMDAvf/AthwIrN5YDVMVvzK9eiF6ZrYKLlulGn+g5OaOtZso0yQnUdmzFj6powVtP0DSz/a", + "37eQQzYTo147+dnHjpQsyZ4CJdMx6vtTv/cEqQp15qjf+57GdmuGTw5Wf1IPlXuy/3j1Ry9ldsbjmKEh", + "oYr5nGYLJ/i4yFo3Ub2B/+7pJoOxTQzItlbql4NCH4hcaI+2lUzwVM2FBaJGKISVlXeMVcWC8jiwqXHG", + "xvhexoutrSnSUXEkfKpuYDb1tcpVB9vlqhADoXvC6KTPkH/sEiPSwKYM9Knf3Kv2PsJ/X8WfkLESFkJv", + "OpGTHPP3Sw/GgvC4yWf4kuOz2g4GOgyCc50KM9336nzSVaeZ+P+msnoSqq0N+AefBwPoL56s/sJWqaxx", + "THPFrqB3gsbMDyxfwQtTlt8GRti/Lv3yZbJVv/fkoMNQfpCC1Xiw5JCr7HlFgPcw8K8sqdbGgWhr3hAT", + "bn+HDYTAddphr00CnAfjXhB8QbDsusvte49m0Yyfw+YdthMP8QVPasw5tyk3pq07pb3NufQuGAeOEyps", + "sCvGTIuzhKtZO2Me4wtdGNO0dc+YXyZjOk7YCWOm6QrXHNxAJgmLiX63zTunm9mKb26nPJWmd829guvS", + "5JRD/eB9gBn2PtI0NSff9iOOqLJFyzEnTbtpI93hbdZFZTxfUCGl6V1QQ7DusKIduclcZ+xhZBhTex/N", + "Xyu5y92EmA/0SebVc7hI+tUrsj0pksS9Yu85CBdRUoA3G4DX4HKvb3NfMeNY9bEquuqPBBVxGdBaSY0k", + "StBUzWQe5m7TcScOdwP/DLj8lRlXgNNflXN9J7i9zodB3re3ZCcmGLohByY809xXt2+05XsQ7o0XTSbO", + "zV0NLlTO5i2bsNfRDq/JPDKveFMWyZjVL8LMZdn9FRhcgXWZa5gsaauVeV9o1lmkcKBBdSmTRF7oMeKN", + "MSz/M/Ph7/rV93/H+MrtXawdOXJu+nLNhiLfMQOwohOayqtcHzwrQCkgQRO7Oa04PZSt7xnOaT3Pugs5", "fNESttBCCqJt1JkJdsDAL82sC1lkRF4I8+FI2C/9OHSSFlkqFVOtl3z49cDFyu/yus+F10OfN3Tv58Kw", - "fVpCjF5948u/EKwx2LXw/d4n293r+PNeJFU+OLMBjkt2fKlygMdQJp6xFIpXMqsNhDMFGTYZsxG+LmNY", - "eA0BpGrMJ5Dfk5NTNpmwEjfwFBAL26x4j+4up9pyyFc91rbuf+W4uu5/5RdnCzLh1KnABYbjLd8NLeD0", - "7/pLAAD48Pf3Jy+2uCFKlX+vyeuyH/ZvnwvF0M/Vnd5Hr2Y71GR7OwpoZaCL7ZA3lEnrlmjEnV+3uH/Y", - "6eZrOfSG911LRnDLtQ/vwG7r2G43Gy3me/kbadDyta/t1PA1nWwrQhRs3Zr964WIPljBzgpeOfF1I9h+", - "cLaADLRuJvBHtvjw9/liEJ8NALF5azawoebmTWAk5N6ZwKVyCCko+/SDt8cuMSFh9XdpO1aT3G/KajRD", - "DdqLJgf3jliKWONyKWO0bEra6sM/G6GiwQBQr78uxy3b9EMY6HbP5zjU7qvfD588piy/PSu6fyMa4J5c", - "BK3BKSYysx5xqVh2s8yyq7jLjbarm2HWhzjMljhMmJat7oX27r/VavN158C+fGd1KKI/tbOmj0Z1z1Sq", - "u8AxZTU21K+xfz6/SYbatZ4NIKDdrMpdh7cfNPCySPjNRGIddbxH03RgoezWkaSB+/AOiVQLkuvNiFMD", - "KzAYZxgGjX2Qpi7SRNN0BxKFSLZ70YxFH2WRD5RBwO8QCPG7AaE9Mt+SE/z2wyNbbySWkRpiD1BuxJTH", - "UK67xyMRRGbEPhShjcYRGF0mCYsA9MVW3JizfCbjKvZohtEWZvzoRzbjM/EaWKh31FMsL9JRj8xlzPoG", - "2Mt0olwXJvZyJC54PtMkRTOaTW3hErdefD5nMac5SxbYpWmIxXViXXENi7w1KfIiqxYTtcsP0/JKZmQm", - "lW7KzqAdkOqTjMU8Y5Hv6Deoc87t/P7Xnw2qF5ufsThmsfd9oRCnKEo4E/lYsSjDohZc8JzThP/JDO7w", - "8H9h3hayyEbCUx0rgldYNkBmGNTZ7W6o5drJAufKeEXNgA0X36xz9DBNl9KmiiQPHongdfNp6KMvy6N6", - "jTrd6MwWhbkTjZ7KLKdJd31uabNq7Bi+tySC+nmv2KRItEZwqqai+Yy2aWkplxbSJ58xno1EVRuqPsEC", - "OPi4ATdLRUxoFOk/8QUbOD/jKpfZYjgSb0WyMLpOaVXXgFCvY/pyZfHUc0koUQ5UXfdWbh2d1Vp1zu++", - "UrP3cTDsW6nawhR2UnDtnz6ouU5qzokdigVR29R2cPZanZFgb/rw7UYGjn3hr8q+gtUgacbK+sksJlQR", - "xgGecJLQnEwYg2JigFY2wPpgtou21AajKSzd24rz2KlKaYkjMTO1NJhhrUCSStDIgJwaALmxAbeEwE94", - "4NDjvQe3JcjDYzEvksPM1dnCZnCtit88NcEc+PqH3yX748PfzQT1sYL26RZjO5C+jsGc1YG/vEz1pgxZ", - "bIhSiJF7rvJv7Gqs1ljCVFHzOCFjNBnnfM7GIFOnz4lpHaQUiPyr5jiaDKDmOrzVBqDG4NPKPKwDkWnm", - "BAkI1WCuMsROI16AlIdQ1RWhqjU1v2qH2Ua8aq3P1UfEVsX/JR4KreGoh3TDgT5AQ/AwB0/uTpCP4bSt", - "MHe3IxZUGVR7NP7fQuXguFuCQ2OrFZZv2zQPG6g5px8ZwTqt3lsKba/yJDYSpqUzmlAR1Y8ThWKDiCqm", - "zNaBhRIimWl7FI+XYUGERgdlx3dKEmFwh25sNyyTNWpaTK3KO3fcpgoCPzZF5vqF2wjZCjyDmkQG8QTK", - "nQ4lzbZ8vWLWD5f7x3NkZRTaqAOob66wMEZO5ykoG1PmVJVFW6BONrzVNabctdj5OP6C5uwdn7OgybGF", - "4/4PLEeh+x7Hf9PR3BVlYWhS7arCvnF/wBt850SFc69fS2AJ4hXOlnq14pAbBFWDae7aNcNWcmmsve8P", - "9zowvlEMfoD+bjwTo6TlwThdYZz6fHIdlilChvvdDslh5d+la1MBPF+SSOQCvR+mGUspj+0pvHY8H644", - "XkP7d+9kDZx+Gw7VQEj7JgmP7/71xP53qz84kmKScK0Iw2fvupDc1Ka69ykql241slhFrsPn8JuTxJb7", - "CH98X0LUc3dJuz9n0VsoKXuK5XnCtN28Zwu5tzuoTFgdFGIzgQfl9/ZyRk70hmgbSxZkUoiYxVWpM6EQ", - "aDkyEaeSC4iTUgsRzTIp+J+1fnLdc7Vt9/CC57ORgBrZgI1GlMQ7yIydM1Hoo2Qkp4IjtpBwtJgikzzh", - "+QIAAOGS8jKFSLNW0HRfOQwsLYNyIu6owthFCG5cPR28NJN54ubyhuPbu2uxe4RFbINr/dOwE8mGPrh+", - "TZdnVCgKjuxu5rb/gcNN8SJpbbQDFYTP5wUQ1iegpmQipzyiCWiYDOpKmkbn8hymQT2vKkCFcKORFKqY", - "l78OyTufCgyeKO1hrcgyxWqdAkLDSJwtLP7DcpdBZWJum+PgqMiUzNZ1HVSW7tocCP5S3Q43gkdRJ0cC", - "zvb9dSXUROGaNBSEcqB2HGAcZjdIm0Hgw7uTKOnjo7wsB3oI47RM3JYQA4taC0PDCB8yYTQvMoZhrxjv", - "inN3b2I5PLYhjm2avO5NejN20Owsai+nlwMol7s0oVdmUyr4n/DjwHw7KD/dIRu99Xo2t0+mUG/wRmTJ", - "6/fFJPXXyiFIuarIQVax13rd3axLMgw78MqujI4lq39DJscW2fdOJwWGTJHdM3JVJ2LV5VUlU6YMsszw", - "5ZaTua39PDBNrhvSvOapGXvpCqFm3j5bEGVKYXcETjWvf/gd4m7tIeZge8fwl0DZRhG3EH69ah66Yfjl", - "vIndZ5e9jMPOZVZi+f02Y4LIOc9zFvctGdrYUw7qH5oFHL5T8mgulRbkSO/gE56p/PGQQBsUvtAzzpKY", - "cEXSTJ5zbWbazEpq4AL7hCMuoPLA/YbkME2ZiQ32wQVHIpdmzPbdPjHppYgfaFEI7Xteo9ccRr7LQ+lr", - "s47AaXfYpGqeHl05eqeSrPZ0teFR/NaqxYzzCT4UaFfz1BnNoxmREysHpVbR3HaUyAKnXxl8ytYMWRS6", - "gDLtdoCAmvz4zd/W4xI7I0AmTGZLuwMYa6D1TkH1tW7q0fS1bpudULF4O2ndTtp62Q5xHzqdo540Oebd", - "jFnGmNFzRs4YE+WuCqmMmf7VJCNq6wayjMwFgixUsvhyRA/lYx3hq51JrLW7/FRCk8TZxS1HEtfQDmF0", - "nWl+xR34I1uc+iVjqnlQta3ZuOgfYHW9A+HqhageCd37Zwvk1TGPu54K7fsmH2tU7O8/jXgM/2XbOxu+", - "QhJv2i/ryLhXOLue8mjaXK/sww+bRGSZplujp8zznQLzmhHcUBST6T3EPebRl5+pU67imvwT3g33Ppm/", - "GuC8IeTbksva6qYbEN+SytVOeEfAA4TvTiB8N+aY/rIYsRV8MGX5bWGC/evULw9oDU03+hUYMNVWYWuc", - "VZ0LTSGNPFkQKRIsClkIno+h/gZ6gmy+IJ51W2OYbo53d+XR32RrvlbRuX8RQjvcy6G82h4aC63himAL", - "4eWwFhA5WXaKhLasXEBZpDshHDDbMBGeZFyDJBxJlZtu25CC3oGZCUSRGVVEFVHEWKy11p2VDGRJq9YN", - "l11JOqbynGWCioh1Ewfbt8EBw3C3hKN8lECP1kPiYnMzpmRyzhRhNJqVtw08ZiLnE47QZWXgHDjoshIm", - "aCRMhyZK2EJWGgZgsTMd+yRNCs8VU83bGwk/aBfIHL9gik8FulzOGImwJL0UWtz5JajcScbUjMA13zlN", - "bESI8VPYVSNcjYR+B8L1bGPRjMXDcYu+KGe/NS5nsxu8HamDHxy9dZ1wnbtlg4p2t8iXqSQCMl9yyrJQ", - "nHJm2i6m04yf05x1vKlOkjnsZXu63YzHq/zDKcsGemdTKY0YSTMeMeI+bXEY2z4GZR/hnfPqvr2ff36j", - "N5ZjTdeXWj0TiL9nbsGff35jzmAeizS5X7+m13czB+Ey3m31GjaYd0f+Q8O5b00vSPR1OxB98QkyG8zZ", - "F+9DbHDb+sy2SovufQL+6upVXI81jZMxxJqrDRJD14OzcSfOxt2xFqzbit15msgzmpRE4DdDYhNU8N9Y", - "/tuxKgF9oY/qE0LFYtUmbuhosFrw+tAQsL3Lu002+MD9spmILdVoNcZONrQ/zGXMEv2v2g1zrc567bdc", - "3st754fz1y06fznp3qbSqm6GSy5VbBhXjR5ytiCvX5RqDPJ+4UGrJhuJoCqbsromu9k9c//ajm730Zmm", - "marKSVdlbMsmyzZhfGdZPPdOQ6fwix0ETtHpNGNTIOAhkmr9SKpV61KNozJvb1SaHL4dfGSL7R27QFBu", - "PJUVqLhnO7NTGJU4z2oh8hZ/B3za6tiApzsNhgJKbygUCvoOcQWq/C/ehWFXr8EVof1q7xP8t6szooVv", - "jNfB9rz61GQ6ffA07MTT0MoBS8OW4Ctzng6ejm/B8u5flxa4J2m8SzjFZN+2RBe1KAITInQznLKr8KD1", - "N6trY9P7FxjUxrEd7bNyv+sW8mDV4kRmpugJwOGznJweRhFL8+ekvrin5JFntTzWJsgUXRh5VkR5kbGY", - "/PPk7S/++b7SYM4u871InZ/qT2N5IRJJ8ZCv6JxBEVJtGlFydPIvAoXUVMFh4JrMkVBpxmisZozlpuqm", - "fjGSSTEXqq+tC7B++s6oO51kct4nuewTm3/b/0B+t/EYYx73XXDG+CNbeP/SYtz/QDAhI+ZzJqDc3XA4", - "xNyMPhabKW090/6poUcbagxzWjEw8WLGhPcWV9YaguX6qxqJ02kmi3R8thiX/Z3iOPNZxhg5ddT9l+0G", - "E2VtR7mcMqgmpXscCezSG22gWxLutSW0467ov2AE2LWrv2osWL9n5UN/zC7pPE2w4x/0CmEidCWUqFww", - "6LjcAFe/3+8B+2rLWItILvu+UFRkoioS4Obv54uU9aGFkXiy/+TpYP9gsH/wbn//OfzvP/3ajwfw4/7B", - "D19/9Z9vvvrq8NVvhz/9+PLgyS//3j/6n+9e/din0ZwNuIj6h9GckdciGvanaT54NsiL7Ez2uUiLvH/w", - "pNHbQai3J1vp7cl+o7cnod6eVnv7/um//3Pw06+H3/327b++OT558qI/TeQZu+z/AP8hRzJLK73JItfd", - "PdP7yC+SgDgOzhatq9vyTnNF116f9eZ3vfl5hrueEw6TEKvyjIvpg/PWj4ra+CSQJlR0yHWF11p8tdjE", - "Dl210MG2LiGdx/acZWo9H+0t8ZCumI7teTOPdUc37czURNwzX6YVqOYVzHEm4yLKyRHNaSKnm8V3CXYB", - "XbS6PPXDnXo89ZreLIK9piDIQAkVX77X0yzgZvwT2B/2Pun/dA7S0nO4PO/TENjhRhn6fXCN7sQ1eiU2", - "Weo+XcYCU5bf/PrvX6tCecj2bHpbr8h8yz2yy/jPuGVvggV34ZRVLMvX3k+vl/3vNI7i5lJguHVnm/Ue", - "jeOV0O00jgcAlK6UjDgceyBYjLYcEZ3FNzCtX58I3cYYyISKQz0PD5XHlkMpWzabyOzKmj9c8zeOoX4Y", - "9IMplCttHGThL38TKI0q4MUbsqxc/237ADy8T7V1gScdQ+5az+O/YZJXWGq/srk8Z564TDI5bxUYz2S7", - "doHpt7ZtxvlgGW6XZS1reHyxMyvRsZ89e3DdJeTRhznRWo73gQ33r1sv35fycWGW26U1ujafexbqHWX1", - "XRrC65+Brl3W7l1hgV1I3fIjURbN+Dlrj2g6xBesr8hcBjZl0TR03zyW9yiWznKCzwe7Ycq0OEu4mrUz", - "5TG+sJIpTUMPTHlnmdJywi6YMpMTnqxKkT/D9SD27RZPoHlt4BrdShjIdTAaEnzPIgvqqxpkKTND5ITl", - "ORfTTjlUgl3UG4fg4O9rHRK9htSErCjTAZwF7NemqGeeyUQRLs4lj9hITJkwvDckh6JaqCiiAisbzIsk", - "52nCGsMkMZtwweIhORyJ2kPCFUm4+Ij5kV5uNU3TIXk346pybOGKMBAurmYsHom4yGyhjVrDf1Xo67K1", - "kjM2p1yosvRpq2+yJlQ7DcWoisMNB2WY8QbEr/rGlx+hEZSYrtIY1ud7n3jHAI2QoL4VyYKoIpo1hcdA", - "4cbGCwYFwcvAOiHzSmKy+cw+SrkwiIJUeKHFhdJduH86aAL9GYS+g0BPuKCJnnarCFSbd7IpMqsPRfzB", - "e7ibuBK6KWMvjSmpc23QPXhb+GD/5nTjfXHkbc5ky2NHVvKZcc/dIKvtyn12hcPADTL8/cv7o9s5Oaji", - "zE3rigra1Vd3GOtf6WhbMf/UAxOrhffTe40pFswvqCzBFtMJTvx2bzqtwCfmIXhnefBOXfibqqaytBV/", - "QdDA9RvckXVrlJ9P2M1gxQYICbGW//yOx8joD75b/cGRFJOER3nYgK6x0GqWXLL17X3y/1mF32uaGLWe", - "Vx/6qo1/AbbGWrx6T8yNnfJbp0hdffbA97ACi99Gi3fef2WdeN3tcmx/V+dDOx0bnBJHInBMfADqu+7q", - "4L7QPMRRr3kUI06iV2qjlZeCnfRT9Xc/0nSjIL/lCqy+1a4RCbV19bWijy8gCLAhaKv29YegwGztPf+q", - "UhZREbGkPTzkCJ7j9lIRHvIbTxK9WHrD4ULLVjRjcQHun8icowmf6C8zRmjGRkIC6E/V3WK+MjtQTvWW", - "OdEDhdpv0DvOTs7noetLeONWHJCvx6DE9boZ/+hah/S77SK9okEJq7jjA340o2K6JBzxKJGKKUJJVgih", - "pba60YsYxVGZi2MpoPqVzOBYmEusDGdDF0ycxJEpYapPlADZlk4zGjPVBxw1+7duG6KMkMRAoA8+uEdi", - "jWt182KNhNy9qm7XLeAwjTsW8ELYzXPgbZTtAv/evd/cz/2tNnD9GO7pwRP1sMm1y0DJbq2ctoFM5PRy", - "EMlqMcSAG6p8bTcXh69FlBSxF6FELwn0F8LG6uI64djg2DTYC4DQnUmZMCqu113yjl4eyfi+Ba265Qxy", - "6Dt62VbiszV1PHgpZbl0p+GWZgVvNs7SEBE8UOCjLz/A0jLNFXmmRd/tfcpxohqZ1sHoRI+1Vm/SruWH", - "6MSdRCduiTP67TeEt2W5929AcdwTj+HWmMjEItYjDBXLbpSPdhVhuMn+dxNs/ABZ1QJZBdOyrc1Vt82y", - "83Bp1J9lRJNev1dkSe95b5bn6fO9vUT/OJMqf/4plVn+eY+mfO/8KSBZZ1y3rdDmzozNDZewvee9b7/9", - "9ltY8EbOJSbQYSjDFO2gskv1fG/vE/7+eUhTPvwoxXT2xzCS80C3poFKx4U+tjJRzPUs4T+KXr9H9f/N", - "GUbXfQgRVs7oUSKLuEGWO5YMI/3czoSWX7MojQu5snQ+O6dJgb58OXF5GIrkkkQzFn3UZhPPyITRvMhs", - "pf1hqW2ClfYDY/DytAYJO2eJuxePpJjwaZE5L0ej5Rf4puq1LhqJMOuRzKmgU6YQObdv0abQuYkj8e52", - "VONyZ3BGFYttUG2QmHqeZZMmV0ExpjnVDRKsd8vFlAiZzU0mS5rxSP8ElSs0IQkV00IbagCTrwiNMqkU", - "scVy1ZBgFV+o2qAWImIxArO4dDd2iYJGlCwyeFPEhBa5HMAkZ3MWYymJfMYWhE4zxoJjdAUeA6GhyAiK", - "ZCzNmGICEn3MGqT0jCc850yRMxp9xCoCuFv1TelRGxyRsmxQCJ7jTK3mAdtvgKR3zsrXE2PjJiKaREVi", - "LACGS+3YO9iF1lfN1o+KLGMi4mY2gVy9qF1aLL8NNGyjsl3yo+XdQAoh9m1znzr03Aj5bvZ/mKaKMAEV", - "Uhay0HOm+UdzjB4nNM//ZJV8TCj2Qi5k9nGSyAsot6iV+1QvnJjiEpdMuFA5myPpWrtjaW3oNqIC+HKO", - "uCgxYWIG6mghizIDlEUS29D9KIzggYtMn9EgVY0qEIhZJgX/U7+ChIJoAVH5jGfxIKVZvtC6IZ/IbK76", - "jkng8kSzSZ/YBFMz4pgl/JxBaqed/j6ZURHj8tDFXItAJJOEwd6BqgbvVG1iSMYSiu4o9TG8XHpSAkv0", - "UuQ8T5juosbcmBdr1LH+ZWLlcjVv+K2GoqsqN8F+r3lGo49mauUE18oKv1akuMbDqq/QZg1yEfNzHhc0", - "UfplP3FXYSqhftEo4zNmgdKQfSAJsDnY4PCqnsqQQNs9bpOxlV9f97hcz4ExwRsgMucNXuk6spflp2km", - "NUksJtSKlSxUstByqLWTVelK4k4ypwvI8dTTMZ+zmNOcJQtCzylPbAUmrBlU3VUd2dh328CUC4KbyQvI", - "IDWVdZkdbz1vnAqaLHIeKZIWWSqVVjymKbNsdsexMKduD/Wq9upxzmSMSwVlU7QqHZI39t15tUnjKtPE", - "uLpUQCCBkjeobDWJk4Rd8jPbAGwCERM041LVZ0f1Pn/4/H8CAAD//3gbssRZuQQA", + "fVpCjF594/O/EKwx2LXw/d5H292r+NNeJFU+OLMBjkt2fKlygMdQJp6xFIqXMqsNhDMFGTYZsxG+LmNY", + "eA0BMnvMJ5Dfk5NTBniTFsTvFDBl207xHt1drNpyyFc1a1v3v3JcXfe/8ouzBZlw6lTgAsPxlu+GtmbF", + "7/pLAAB4//d3J8+3uCFKlX+vyeuyH/ZvnwvF0M/VF72PXu3sUJPt7SiglYEutkPeUCatW6IRd37d4v5+", + "p5uv5dAb3nctGcEt1z78AnZbx3a72Wgx38vfSIMnX/vaTg++ppNtRYjCWbd2/vVCRO9Pwe4UvHLi64dg", + "+8HZAjLQuh2BP7DF+7/PF4P4bACY+ls7Axtqbv4IjITcuSNwqRxCCso+fe/tsUuOkLD6uzw7VpPcb+rU", + "aIYaPC+aHNwv5KSIZbKXMkbLpqRPffhnI1Q0GADq9dfF3LJN34eBbtc+x6F2X/1+2PKYsvz2rOj+jWiA", + "O3IRtAanmMjMesSlYtnNMsuu4i432q5uhlnv4zBb4jBhWra6F9q7/9ZTm687B/blL1aHIvpTO2v6aFR3", + "TKW6CxxTCmRD/Rr79vlNMtSu9WwAAe1mVe46vH2vgZdFwm8mEuuo4z2apgMLZbeOJA3ch1+QSLUgud6M", + "ODWwAoNxhmHQ2Htp6iJNNE13IFGIZLsXzVj0QRb5QBkE/A6BEL8bENoj8y05wW/fP7D1RmIZqSH2AOVG", + "THkM5bp7OBJBZEbsQxHaaByB0WWSsAhAX2zFjTnLZzKuYo9mGG1hxo9+ZDM+E6+Btf5HPcXyIh31yFzG", + "rG+AvUwnynVhYi9H4oLnM01SNKPZ1BYucevF53MWc5qzZIFdmoZYXCfWFdewyFuTIi+yWoVKs1AwLS9l", + "RmZS6absDNoBqT7JWMwzFvmOfoM659zO73792aB6sfkZi2MWe98XCnGKooQzkY8VizIsasEFzzlN+J/M", + "4A4P/xfmbSGLbCQ81bEieIVlA2SGQZ3dvgy1XLMscK6MV9QM2HDxzTpHD9N0KW2qSPKgSQSvm09DH31e", + "HtVr1OlGZ7YozJ1o9FRmOU2663NLm1Vjx/C9JRHUzzvFJkWiNYJTNRXNZ7RNS0u5tJA++YzxbCSq2lD1", + "CRbAwccNuFkqYkKjSP+JL9jA+RlXucwWw5F4I5KF0XVKq7oGhHod05cri6eeS0KJcqDqurdy6+is1qpz", + "/uUrNXsfB8O+laotTGEnBdf+6b2a66TmnNihWBC1TW0HttfqjAR704dvNzJw7AtfKfsKVoOkGSvrJLOY", + "UEUYB3jCSUJzMmEMiokBWtkA64PZLtpSG4ymsHRvK85jpyqlJY7EzNTSYIa1AkkqQSMDcmoA5MYG3BIC", + "P+GBQ4/3HtyWIA+PxbxIDjNXZwubwbUqfvPUBHPg6+9/l+yP9383E9THctanW4ztQPo6BnNWB/7iMtWb", + "MmSxIUohRu65yr+xq7FaYwlTRc3jhIzRZJzzORuDTJ0+I6Z1kFIg8ivNcTQZ6LcQJ7ANQI3Bp5V5WAci", + "08wJEhCqG11liJ1GvAAp96GqK0JVa2p+1Q6zjXjVWp+rTcRWxf85GoX24KiHdMOBPkBD0JiDJ19OkI/h", + "tK0wdzcTC6oMqj0a/2+hcnDcLcGhsdUKy7dtmocN1JzTD4xgnVbvLYVnr9ISGwnT0hlNqIjq5kSh2CCi", + "iimzdWChhEhm+jyK5mVYEKHRQdnxFyWJMLhDN7YblskaNS1Hrco7X/iZKgj82BSZ6xduI2Qr8AxqEhnE", + "Eyh3OpQ02/L1ilk/XO4f7cjKKPShDqC+ucLCGDmdp6BsTJlTVRZtgTrZ8FbXmHLXYmdz/DnN2Vs+Z8Ej", + "xxbM/R9YjkL3PY7/pqO5K8rC0KTaVYV94+6AN/jOiQrnXr+WwBLEK5wt9WrFITcIqgbT3LVrhq3k0tjz", + "vj/c68D4RjH4Afq78UyMkpb7w+mKw6nPJ9dxMkXIcL/bITms/Lt0bSqA50sSiVyg98M0YynlsbXCa+b5", + "cIV5De1/eZY1cPptMKqBkPZNEh5/+dcT+9+t/uBIiknCtSIM2951IbmpTXXvY1Qu3WpksYpch+3wm5PE", + "lvsIf3yfQ9Rzd0m7O7boLZSUPcXyPGH63LxnC7m3O6hMWB0UYjOBB+X39nJGTvSGaBtLFmRSiJjFVakz", + "oRB4cmQiTiUXECelFiKaZVLwP2v95Lrnatvu4QXPZyMBNbIBG40oiXeQGTtnotCmZCSngiO2kHC0mCKT", + "POH5AgAA4ZLyMoVIs1bQdF85DCwtg3IivlCFsYsQ3LhqHbwwk3ni5vKG49u7a7E7hEVsg2t9a9iJZEMf", + "XL+myzMqFAVHdrfjtv+Bw03xImlttAMVhM/nBRDWJ6CmZCKnPKIJaJgM6kqaRufyHKZBPasqQIVwo5EU", + "qpiXvw7JW58KDJ4oz8NakWWK1ToFhIaROFtY/IflLoPKxNw2x8FRkSmZres6qCzdtTkQ/KW6HW4Ej6JO", + "jgSc7bvrSqiJwjVpKAjlQO04wDjMbpA2g8CHX06ipI+P8qIc6CGM0zJxW0IMLGotDA0jfMiE0bzIGIa9", + "Yrwrzt2dieXw2IY4tmnyujfpzdhBs7OovZxeDqBc7tKEXplNqeB/wo8D8+2g/HSHbPTG69ncPplCvcEb", + "kSWv35Ujqb9WDkHKVUUOsoq91uvuZl2SYdiBV3Z16Fiy+jd05Ngi+37RSYGho8juGbmqE7Hq8qqSKVMG", + "WWb4cotlbms/D0yT64Y0r2k1Yy9dIdTM22cLokwp7I7Aqeb1979D3K01Yg62Z4a/AMo2iriF8OtV89AN", + "wy/nTew+u+xlHHYusxLL77cZE0TOeZ6zuG/J0Ic95aD+oVnA4TslD+ZSaUGO9A4+4ZnKHw4JtEHhCz3j", + "LIkJVyTN5DnXx0ybWUkNXGCfcMQFVB6435AcpikzscE+uOBI5NKM2b7bJya9FPEDLQqhfc9r9JrDyHdp", + "lL4y6wic9gUfqZrWoytH71SS1Z6uNjyK31q1mHE+wYcC7WqeOqN5NCNyYuWg1Cqa244SWeD0K4NP2Zoh", + "i0IXUKbdDAioyY/f/HU9LrEzAmTCZLa0O4CxBlrvFFRf66YeTV/rttkJFYs3k9btpK2X7RD3vpMd9ajJ", + "MW9nzDLGjJ4zcsaYKHdVSGXM9K8mGVGfbiDLyFwgyEIli89H9FA+1hG+mk1iT7vLrRKaJO5c3GKSuIZ2", + "CKPrjuZX3IE/sMWpXzKmmgdV25qNi/4eVtczCFcvRNUkdO+fLZBXxzzuahXa900+1qjY338c8Rj+y7Zn", + "G75EEm/aL+vIuFM4u57yaJ65XtqH7zeJyDJNt0ZPmec7BeY1I7ihKCbTe4h7zKPPP1OnXMU1+Se8G+59", + "NH81wHlDyLcll7XVTTcgviWVq53wjoB7CN+dQPhuzDH9ZTFiK/hgyvLbwgT716lf7tEamm70KzBgqk+F", + "rXFWdS40hTTyZEGkSLAoZCF4Pob6G+gJsvmCaOu2xjDdHO/uyqO/ydZ8raJz9yKEdriXQ3m1PTwstIYr", + "wlkIL4e1gMjJMisS2rJyAWWRvgjhgNmGifAk4xok4Uiq3HTbhhT0Fo6ZQBSZUUVUEUWMxVprfbGSgSxp", + "1brhsitJx1Ses0xQEbFu4mD7NjhgGO6WcJSPEujRekhcbG7GlEzOmSKMRrPytoHHTOR8whG6rAycAwdd", + "VsIEjYTp0EQJW8hKwwAsdkfHPkmTwnPFVPP2RsIP2gUyx8+Z4lOBLpczRiIsSS+FFnd+CSp3kjE1I3DN", + "d04TGxFi/BR21QhXI6HfgXA921g0Y/Fw3KIvytlvjcvZ7AZvR+rgB0dvXSdc527ZoKLdLfJ5KomAzJec", + "siwUp5yZtovpNOPnNGcdb6qTZA572Z5uN+PxKv9wyrKB3tlUSiNG0oxHjLhPWxzGto9B2Ud457y6b+/n", + "n1/rjeVY0/W5Vs8E4u+YW/Dnn18bG8xjkSb369f0+m7mIFzGu61ewwbz7sh/aDj3jekFib5uB6IvPkFm", + "gzn77H2IDW5bn9lWadG9j8BfXb2K67GmcTKGWHP1gcTQde9s3ImzcXesBeu2YneeJvKMJiUR+M2Q2AQV", + "/DeW/3asSkBfaFN9QqhYrNrEDR0NVgteHxoCtnd5t8kGH7hfNhOxpRqt5rCTDe0PcxmzRP+rdsNcq7Ne", + "+y2Xd/Le+d7+ukX2l5PubSqt6ma45FLFhnHV6CFnC/LqeanGIO8XHrRqspEIqrIpq2uym90z96/NdLuL", + "zjTNVFVOuipjWzZZtgnjO8viuXcaOoVf7CBwik6nGZsCAfeRVOtHUq1al2oclXl7o9Lk8O3gA1tsz+wC", + "QbnxVFag4o7tzE5hVOI8q4XIW/wd8GmrYwOe7jQYCii9oVAo6DvEFajyP3sXhl29BleE9qu9j/Dfrs6I", + "Fr4xXgfb82qryXR672nYiaehlQOWhi3BV8aeDlrHt2B5969LC9yRNN4lnGKyb1uii1oUgQkRuhlO2VV4", + "0Pqb1bWx6d0LDGrj2I7ns3K/6xbyYNXiRGam6AnA4bOcnB5GEUvzZ6S+uKfkgXdqeaiPIFN0YeRZEeVF", + "xmLyz5M3v/j2faXBnF3me5E6P9WfxvJCJJKika/onEERUn00ouTo5N8ECqmpgsPANZkjodKM0VjNGMtN", + "1U39YiSTYi5UX58u4PTTd4e600km532Syz6x+bf99+R3G48x5nHfBWeMP7CF9y8txv33BBMyYj5nAsrd", + "DYdDzM3oY7GZ8qxn2j819OiDGsOcVgxMvJgx4b3FlT0NwXJ9pUbidJrJIh2fLcZlf6c4znyWMUZOHXX/", + "Y7vBRFnbUS6nDKpJ6R5HArv0RhvoloR7bQnt+FL0XzAC7NrVXzUWrN+z8qE/Zpd0nibY8Q96hTARuhJK", + "VC4YdFxugKvf7/eAffXJWItILvu+UFRkoioS4Obv54uU9aGFkXi0/+jxYP9gsH/wdn//Gfzvv/3ajwfw", + "4/7BD18//e83T58evvzt8KcfXxw8+uU/+0f/+u7lj30azdmAi6h/GM0ZeSWiYX+a5oMng7zIzmSfi7TI", + "+wePGr0dhHp7tJXeHu03ensU6u1xtbfvH//nvwc//Xr43W/f/vub45NHz/vTRJ6xy/4P8B9yJLO00pss", + "ct3dE72P/CIJiOPgbNG6ui3vNFd07fVZb37Xm58nuOs54TAJsSrPuJjeO2/9qKiNLYE0oaJDriu81uKr", + "xSZ26KqFDrZ1Cek8tucsU+v5aG+Jh3TFdGzPm3msO7ppZ6Ym4o75Mq1ANa9gjjMZF1FOjmhOEzndLL5L", + "sAvootXlqR/u1OOp1/RmEew1BUEGSqj4/L2eZgE345/A/rD3Uf+nc5CWnsPleZ+GwA43ytDvvWt0J67R", + "K7HJUvfpMhaYsvzm13//WhXKfbZn09t6ReZb7pFdxn/GLXsTLLgLp6xiWb72fnq97P9F4yhuLgWGW3e2", + "We/ROF4J3U7jeABA6UrJiIPZA8FitMVEdCe+gWn9+kToNsZAJlQc6nm4rzy2HErZstlEZlfW/OGav3EM", + "9cOgH0yhXHnGQRb+/DeB8lAFvHhDJyvXf9s+AA/vUm1d4EnHkLvW8/hvmOQVJ7Vf2VyeM09cJpmctwqM", + "d2S7doHpt7Ztxnl/Mtwuy1rW8PhiZ6dEx37W9uC6S8ijD3OiPTneBTbcv269fFfKx4VZbpen0bX53Duh", + "fqGsvsuD8Po20LXL2p0rLLALqVtuEmXRjJ+z9oimQ3zB+orMZWBTFk1Dd81jeYdi6Swn+HywG6ZMi7OE", + "q1k7Ux7jCyuZ0jR0z5RfLFNaTtgFU2ZywpNVKfJnuB7Evt3iCTSvDVyjWwkDuQ5GQ4LvWGRBfVWDLGVm", + "iJywPOdi2imHSrCLeuMQHPx9rUOi15CakBVlOgBbwH5tinrmmUwU4eJc8oiNxJQJw3tDciiqhYoiKrCy", + "wbxIcp4mrDFMErMJFyweksORqD0kXJGEiw+YH+nlVtM0HZK3M64qZgtXhIFwcTVj8UjERWYLbdQa/kqh", + "r8vWSs7YnHKhytKnrb7JmlDtNBSjKg43HJRhxhsQv+obn3+ERlBiukpjWJ/vfeQdAzRCgvpGJAuiimjW", + "FB4DhRsbLxgUBC8D64TMK4nJ5jP7KOXCIApS4YUWF0p34f7poAn0ZxD6DgI94YImetqtIlBt3smmyKw2", + "ivi993A3cSV0U8ZeGlNS59qge/C28MH+zenGu+LI25zJlseOrOQz4567QVbblfvsCsbADTL83cv7o9ux", + "HFRx5qZ1RQXt6qs7jPWvdLStmH/qgYnVwvvpncYUC+YXVJZgi+kEJ367N51W4BNzH7yzPHinLvxNVVNZ", + "2oq/IHjA9Rvc0enWKD+fsJvBig0QEmIt//kXHiOjP/hu9QdHUkwSHuXhA3SNhVaz5JKtb++j/88q/F7z", + "iFHrebXRV238MzhrrMWrd+S4sVN+6xSpq20PG0YJJVj8Rlrc8/4r6wTsbpdl+7syEAkOaRMzcSQCduI9", + "Ut91lwf3peY+kHpNW4w4iV6pjtaNpPbCqJermYBFt0bM0g62xl1dlDQ49YbuShp0rNqh70LY9VZMSryT", + "qQvYoNt+Hy4ys/QivpNNUP3dj+7eKLB2uTDXzdsbk+T+yj4+g8DbDSX1bgfirmtnX1XKIioilrSHZB3B", + "c7ToKsJDfuNJohdL23hcaNmKZiwuwOUaGUVD+ER/mTFCMzYSEoC2qi5O85Ux+nKqrdSJHijUW4TecXZy", + "Pg+FDMAbt+JQej1OHFyvm7mTWOtg/GVfS1xxx4VV3PGhOppRMV0SAnyUSMUUoSQrhNBSW936RYziqIxh", + "IAVUnJMZnMRyidUYbbiQiU06MmWD9SEOYBLTaUZjpvqAXWj/1m1DZB+SGAiuwwd3SKxxrW5erJGQL6+S", + "4nULOEzjjgW8EHbzHHgbZbvAv3PvN/dzf6sNXPmHe7r3/t5vcu0yULJbK6dtIBM5vRxEslqANOD5LV/b", + "zWX9KxElRexFBdJLAv2F8Oi6eCs5Njg2DfYCwI9nUiaMiuv1UL6ll0cyvmuB4m45gxz6ll529HisuAi2", + "XLrTEGezgjcb22yICBoU+OjzD2q2THNFnmnRd3sfc5yoBrpBMCLYY63Vm7Rr+T4ieCcRwVvijH77rfxt", + "We79G1Acd8RjuDUmMvG/9ahexbIb5aNdRfVusv/dBBvfw8S1wMTBtGxrc9Vts+w8XI74ZxnRpNfvFVnS", + "e9ab5Xn6bG8v0T/OpMqffUxlln/aoynfO38M6PEZ120rPHNn5swNcQ+9Z71vv/32W1jwRp4zJq1i+NAU", + "z0Fll+rZ3t5H/P3TkKZ8+EGK6eyPYSTngW5NA5WOC222MlHM9SzhP4pev0f1/80ZRrS+DxFWzuhRIou4", + "QZYzS4aRfm5nQsuvWZTGhZw81wsiIkbYOU0K9OXLict9UiSXJJqx6IM+NvGMTBjNiwx8jUyBa9Bom5K4", + "stHAyeyNlxs5SNg5S1woSiTFhE+LzHk5Gi0/xzdVr3XRSISZxmROBZ0yhWjVfRuahM5NHIl3t6MalzuD", + "M6pYbAPZg8TUc5ubNLmqpTHNqW6QYI1pLqZEyGxussfSjEf6J6gWowlJqJgW+qAGpSkUoVEmlSK2QLUa", + "EqycDZVS1EJELEYwJJdiyi5R0IiSRQZvipjQIpcDmORszmIs35LP2ILQacZYcIyuqGogHBsZQZGMpRlT", + "TEBynVmDlJ7xhOecKXJGow9YuQN3q74p92vjkVKWDQrBc5yp1Txg+w2Q9Nad8vXE2FCliCZRkZgTAMOl", + "duwd7ELrq2brR0WWMRFxM5tArl7ULi2W3wYatpkQLuHY8m4gbRf7tvmGHXpupFk0+z9MU0WYgKpEC1no", + "OdP8ozlGjxOa53+ySg40FFgiFzL7MEnkBZQ41cp9qhdOTHGJSyZcqJzNkXSt3bGcPXQbUQF8OUcsopgw", + "MQN1tJBFmXXNIolt6H4UBs3BRabPaJAeShUIxCyTgv+pX0FCQbSAqHzGs3iQ0ixfaN2QT2Q2V33HJHB5", + "otmkT2xStxlxzBJ+ziCd2k5/n8yoiHF56GKuRSCSScJg70BVg3eqNhkrYwlFd5T6EF4uPSmBJXohcp4n", + "THdRY27MRTfqWP8ysXK5mjf8VkMBjZWbYL/XPKPRBzO1coJrZYVfK1Jc42HVV2gzdbmI+TmPC5oo/bKf", + "LK8wfVe/aJTxGbPghMg+kHjbHGxweFVPZUig7R63ydjKr697XK7nwJjgDRCZ8wavdB3Zi/LTNJOaJBYT", + "asVKFipZaDnU2smqdCVxJ5nTBeRV6+mYz1nMac6SBaHnlCe26hnW6aruqo5s7LttYMrFnc7kBcT/mWrW", + "zI63jtVABU0WOY8USYsslUorHtOUWTa741hoYbeHepWy9ThnMsalglJFWpUOyWv77rzapHGVaWJcLTgg", + "kECZKVS2msRJwi75mW0ANoGICZpxqeqzo3qf3n/6PwEAAP//bnpKmvnHBAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/addons/convert.go b/api/v3/handlers/addons/convert.go index ae068740a7..38c97c522c 100644 --- a/api/v3/handlers/addons/convert.go +++ b/api/v3/handlers/addons/convert.go @@ -492,7 +492,7 @@ func ToAPIBillingRateCardTaxConfig(tc *productcatalog.TaxConfig) *apiv3.BillingR } if tc.TaxCodeID != nil { - result.Code = apiv3.TaxCodeReference{Id: *tc.TaxCodeID} + result.Code = apiv3.TaxCodeReferenceItem{Id: *tc.TaxCodeID} } return result diff --git a/api/v3/handlers/plans/convert.go b/api/v3/handlers/plans/convert.go index 1b0a893d97..ba4eae02c9 100644 --- a/api/v3/handlers/plans/convert.go +++ b/api/v3/handlers/plans/convert.go @@ -421,7 +421,7 @@ func ToAPIBillingRateCardTaxConfig(c *productcatalog.TaxConfig, tc *taxcode.TaxC } result := &api.BillingRateCardTaxConfig{ - Code: api.TaxCodeReference{ + Code: api.TaxCodeReferenceItem{ Id: tc.ID, }, } diff --git a/api/v3/handlers/plans/convert_test.go b/api/v3/handlers/plans/convert_test.go index 6c85d0621d..508bc10508 100644 --- a/api/v3/handlers/plans/convert_test.go +++ b/api/v3/handlers/plans/convert_test.go @@ -1376,7 +1376,7 @@ func TestToBillingPriceTiers(t *testing.T) { func TestToBillingTaxConfig(t *testing.T) { t.Run("maps code ID", func(t *testing.T) { tc := api.BillingRateCardTaxConfig{ - Code: api.TaxCodeReference{Id: "01TAXCODE000"}, + Code: api.TaxCodeReferenceItem{Id: "01TAXCODE000"}, } result := FromAPIBillingRateCardTaxConfig(tc) @@ -1388,7 +1388,7 @@ func TestToBillingTaxConfig(t *testing.T) { t.Run("maps behavior", func(t *testing.T) { tc := api.BillingRateCardTaxConfig{ - Code: api.TaxCodeReference{Id: "01TAXCODE000"}, + Code: api.TaxCodeReferenceItem{Id: "01TAXCODE000"}, Behavior: lo.ToPtr(api.BillingTaxBehavior("inclusive")), } diff --git a/api/v3/handlers/subscriptions/subscriptionaddons/convert.go b/api/v3/handlers/subscriptions/subscriptionaddons/convert.go index 6a28cf559f..0e4a351303 100644 --- a/api/v3/handlers/subscriptions/subscriptionaddons/convert.go +++ b/api/v3/handlers/subscriptions/subscriptionaddons/convert.go @@ -4,16 +4,21 @@ import ( "context" "errors" "fmt" - "time" "github.com/samber/lo" apiv3 "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/handlers/plans" + "github.com/openmeterio/openmeter/api/v3/handlers/subscriptions" "github.com/openmeterio/openmeter/api/v3/labels" + "github.com/openmeterio/openmeter/openmeter/subscription" subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" + addondiff "github.com/openmeterio/openmeter/openmeter/subscription/addon/diff" + subscriptionworkflow "github.com/openmeterio/openmeter/openmeter/subscription/workflow" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/models" + "github.com/openmeterio/openmeter/pkg/slicesx" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -32,14 +37,33 @@ func FromAPISubscriptionAddonSortField(ctx context.Context, field string) (subsc } } -func toAPISubscriptionAddon(addon subscriptionaddon.SubscriptionAddon) (apiv3.SubscriptionAddon, error) { - now := clock.Now() +func FromAPICreateSubscriptionAddonRequest(req apiv3.CreateSubscriptionAddonRequest) (subscriptionworkflow.AddAddonWorkflowInput, error) { + timing, err := subscriptions.FromAPIBillingSubscriptionEditTiming(req.Timing) + if err != nil { + return subscriptionworkflow.AddAddonWorkflowInput{}, fmt.Errorf("failed to convert timing: %w", err) + } - inst, found := addon.GetInstanceAt(now) - if !found { - return apiv3.SubscriptionAddon{}, models.NewGenericNotFoundError(fmt.Errorf("no instance is active at %s", now.Format(time.RFC3339))) + meta, err := labels.ToMetadata(req.Labels) + if err != nil { + return subscriptionworkflow.AddAddonWorkflowInput{}, err } + return subscriptionworkflow.AddAddonWorkflowInput{ + AddonID: req.Addon.Id, + InitialQuantity: req.Quantity, + Timing: timing, + MetadataModel: models.MetadataModel{ + Metadata: meta, + }, + }, nil +} + +func toAPISubscriptionAddon(view subscription.SubscriptionView, addon subscriptionaddon.SubscriptionAddon) (apiv3.SubscriptionAddon, error) { + now := clock.Now() + + // inst.Quantity is 0 when no instance is active at now (e.g. addon scheduled for next_billing_cycle). + inst, _ := addon.GetInstanceAt(now) + pers := lo.Map(addon.GetInstances(), func(i subscriptionaddon.SubscriptionAddonInstance, _ int) timeutil.OpenPeriod { return i.AsPeriod() }) @@ -52,6 +76,34 @@ func toAPISubscriptionAddon(addon subscriptionaddon.SubscriptionAddon) (apiv3.Su return agg.Union(item) }, pers[0]) + affectedMap := addondiff.GetAffectedItemIDs(view, addon) + + rateCards, err := slicesx.MapWithErr(addon.RateCards, func(r subscriptionaddon.SubscriptionAddonRateCard) (apiv3.SubscriptionAddonRateCard, error) { + rc, err := plans.ToAPIBillingRateCard(r.AddonRateCard.RateCard) + if err != nil { + return apiv3.SubscriptionAddonRateCard{}, fmt.Errorf("failed to convert rate card: %w", err) + } + + // JSON encoders should emit [] not null when no items are affected. + ids := affectedMap[r.AddonRateCard.RateCard.Key()] + if ids == nil { + ids = []string{} + } + + return apiv3.SubscriptionAddonRateCard{ + RateCard: rc, + AffectedSubscriptionItemIds: ids, + }, nil + }) + if err != nil { + return apiv3.SubscriptionAddon{}, fmt.Errorf("failed to convert rate cards: %w", err) + } + + // Addons with no rate cards leave RateCards nil; emit [] so the response satisfies the array schema. + if rateCards == nil { + rateCards = []apiv3.SubscriptionAddonRateCard{} + } + return apiv3.SubscriptionAddon{ Id: addon.ID, Name: addon.Name, @@ -59,7 +111,7 @@ func toAPISubscriptionAddon(addon subscriptionaddon.SubscriptionAddon) (apiv3.Su CreatedAt: addon.CreatedAt, UpdatedAt: addon.UpdatedAt, DeletedAt: addon.DeletedAt, - Addon: apiv3.AddonReferenceItem{ + Addon: apiv3.AddonReference{ Id: addon.Addon.ID, }, Labels: labels.FromMetadata(addon.Metadata), @@ -67,5 +119,13 @@ func toAPISubscriptionAddon(addon subscriptionaddon.SubscriptionAddon) (apiv3.Su QuantityAt: now, ActiveFrom: lo.FromPtrOr(union.From, now), ActiveTo: union.To, + Timeline: lo.Map(addon.GetInstances(), func(i subscriptionaddon.SubscriptionAddonInstance, _ int) apiv3.SubscriptionAddonTimelineSegment { + return apiv3.SubscriptionAddonTimelineSegment{ + Quantity: i.Quantity, + ActiveFrom: i.CadencedModel.ActiveFrom, + ActiveTo: i.CadencedModel.ActiveTo, + } + }), + RateCards: rateCards, }, nil } diff --git a/api/v3/handlers/subscriptions/subscriptionaddons/convert_test.go b/api/v3/handlers/subscriptions/subscriptionaddons/convert_test.go new file mode 100644 index 0000000000..dc3d00b14f --- /dev/null +++ b/api/v3/handlers/subscriptions/subscriptionaddons/convert_test.go @@ -0,0 +1,149 @@ +package subscriptionaddons + +import ( + "testing" + "time" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + apiv3 "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/openmeter/subscription" + subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" + "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/models" + "github.com/openmeterio/openmeter/pkg/timeutil" +) + +func TestFromAPICreateSubscriptionAddonRequest(t *testing.T) { + t.Run("maps immediate timing and labels", func(t *testing.T) { + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("immediate"))) + + labels := apiv3.Labels{"team": "billing"} + req := apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: "01J8GFKQ0000000000000000"}, + Labels: &labels, + Quantity: 2, + Timing: timing, + } + + input, err := FromAPICreateSubscriptionAddonRequest(req) + require.NoError(t, err) + + assert.Equal(t, "01J8GFKQ0000000000000000", input.AddonID) + assert.Equal(t, 2, input.InitialQuantity) + require.NotNil(t, input.Timing.Enum) + assert.Equal(t, subscription.TimingImmediate, *input.Timing.Enum) + assert.Equal(t, models.Metadata{"team": "billing"}, input.Metadata) + }) + + t.Run("maps next_billing_cycle timing", func(t *testing.T) { + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("next_billing_cycle"))) + + req := apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: "addon-id"}, + Quantity: 1, + Timing: timing, + } + + input, err := FromAPICreateSubscriptionAddonRequest(req) + require.NoError(t, err) + require.NotNil(t, input.Timing.Enum) + assert.Equal(t, subscription.TimingNextBillingCycle, *input.Timing.Enum) + assert.Nil(t, input.Metadata) + }) + + t.Run("fails on invalid timing string", func(t *testing.T) { + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("sometime_later"))) + + req := apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: "addon-id"}, + Quantity: 1, + Timing: timing, + } + + _, err := FromAPICreateSubscriptionAddonRequest(req) + require.Error(t, err) + }) +} + +// newTestSubscriptionAddon builds a SubscriptionAddon with a single quantity at activeFrom, +// no rate cards, and no soft-delete. The view stays empty — toAPISubscriptionAddon's only +// dependency on the view is through GetAffectedItemIDs, which returns an empty map for an +// empty view, exercising the nil-slice fallback path on rate cards. +func newTestSubscriptionAddon(activeFrom time.Time, qty int) subscriptionaddon.SubscriptionAddon { + now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + + return subscriptionaddon.SubscriptionAddon{ + NamespacedID: models.NamespacedID{Namespace: "ns", ID: "01J8GFKQ0000000000000000"}, + ManagedModel: models.ManagedModel{CreatedAt: now, UpdatedAt: now}, + Name: "Test addon", + Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{ + subscriptionaddon.SubscriptionAddonQuantity{ + ActiveFrom: activeFrom, + Quantity: qty, + }.AsTimed(), + }), + } +} + +func TestToAPISubscriptionAddon(t *testing.T) { + now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + clock.FreezeTime(now) + defer clock.UnFreeze() + + t.Run("active instance maps quantity from current instance", func(t *testing.T) { + addon := newTestSubscriptionAddon(now.Add(-time.Hour), 3) + + got, err := toAPISubscriptionAddon(subscription.SubscriptionView{}, addon) + require.NoError(t, err) + + assert.Equal(t, 3, got.Quantity) + assert.Equal(t, now, got.QuantityAt) + assert.Empty(t, got.RateCards) + require.Len(t, got.Timeline, 1) + assert.Equal(t, 3, got.Timeline[0].Quantity) + }) + + t.Run("future-scheduled instance returns quantity 0 instead of error", func(t *testing.T) { + // Reproduces the next_billing_cycle case where active_from is in the future: + // the previous code returned NotFound; the fix surfaces quantity 0. + addon := newTestSubscriptionAddon(now.Add(time.Hour), 5) + + got, err := toAPISubscriptionAddon(subscription.SubscriptionView{}, addon) + require.NoError(t, err) + + assert.Equal(t, 0, got.Quantity) + assert.Equal(t, now, got.QuantityAt) + require.Len(t, got.Timeline, 1) + assert.Equal(t, 5, got.Timeline[0].Quantity) + assert.Equal(t, now.Add(time.Hour), got.ActiveFrom) + }) + + t.Run("errors when addon has no instances", func(t *testing.T) { + addon := subscriptionaddon.SubscriptionAddon{ + NamespacedID: models.NamespacedID{Namespace: "ns", ID: "id"}, + Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{}), + } + + _, err := toAPISubscriptionAddon(subscription.SubscriptionView{}, addon) + require.Error(t, err) + }) + + t.Run("preserves description and labels round-trip", func(t *testing.T) { + addon := newTestSubscriptionAddon(now.Add(-time.Hour), 1) + addon.Description = lo.ToPtr("with desc") + addon.Metadata = models.Metadata{"team": "billing"} + + got, err := toAPISubscriptionAddon(subscription.SubscriptionView{}, addon) + require.NoError(t, err) + + assert.Equal(t, lo.ToPtr("with desc"), got.Description) + require.NotNil(t, got.Labels) + assert.Equal(t, "billing", (*got.Labels)["team"]) + }) +} diff --git a/api/v3/handlers/subscriptions/subscriptionaddons/create.go b/api/v3/handlers/subscriptions/subscriptionaddons/create.go new file mode 100644 index 0000000000..02497f1d87 --- /dev/null +++ b/api/v3/handlers/subscriptions/subscriptionaddons/create.go @@ -0,0 +1,67 @@ +package subscriptionaddons + +import ( + "context" + "net/http" + + apiv3 "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/request" + subscriptionworkflow "github.com/openmeterio/openmeter/openmeter/subscription/workflow" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" + "github.com/openmeterio/openmeter/pkg/models" +) + +type ( + CreateSubscriptionAddonRequest = struct { + SubscriptionID models.NamespacedID + AddonInput subscriptionworkflow.AddAddonWorkflowInput + } + CreateSubscriptionAddonResponse = apiv3.SubscriptionAddon + CreateSubscriptionAddonParams = string + CreateSubscriptionAddonHandler = httptransport.HandlerWithArgs[CreateSubscriptionAddonRequest, CreateSubscriptionAddonResponse, CreateSubscriptionAddonParams] +) + +func (h *handler) CreateSubscriptionAddon() CreateSubscriptionAddonHandler { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, r *http.Request, subscriptionID CreateSubscriptionAddonParams) (CreateSubscriptionAddonRequest, error) { + body := apiv3.CreateSubscriptionAddonRequest{} + if err := request.ParseBody(r, &body); err != nil { + return CreateSubscriptionAddonRequest{}, err + } + + ns, err := h.resolveNamespace(ctx) + if err != nil { + return CreateSubscriptionAddonRequest{}, err + } + + addonInput, err := FromAPICreateSubscriptionAddonRequest(body) + if err != nil { + return CreateSubscriptionAddonRequest{}, err + } + + return CreateSubscriptionAddonRequest{ + SubscriptionID: models.NamespacedID{ + Namespace: ns, + ID: subscriptionID, + }, + AddonInput: addonInput, + }, nil + }, + func(ctx context.Context, request CreateSubscriptionAddonRequest) (CreateSubscriptionAddonResponse, error) { + view, added, err := h.subscriptionWorkflowService.AddAddon(ctx, request.SubscriptionID, request.AddonInput) + if err != nil { + return CreateSubscriptionAddonResponse{}, err + } + + return toAPISubscriptionAddon(view, added) + }, + commonhttp.JSONResponseEncoderWithStatus[CreateSubscriptionAddonResponse](http.StatusCreated), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("create-subscription-addon"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/subscriptions/subscriptionaddons/get.go b/api/v3/handlers/subscriptions/subscriptionaddons/get.go index 2858d1f157..970e650b77 100644 --- a/api/v3/handlers/subscriptions/subscriptionaddons/get.go +++ b/api/v3/handlers/subscriptions/subscriptionaddons/get.go @@ -48,7 +48,15 @@ func (h *handler) GetSubscriptionAddon() GetSubscriptionAddonHandler { return GetSubscriptionAddonResponse{}, models.NewGenericNotFoundError(errors.New("subscription addon not found")) } - return toAPISubscriptionAddon(*a) + view, err := h.subscriptionService.GetView(ctx, models.NamespacedID{ + Namespace: request.NamespacedID.Namespace, + ID: request.SubscriptionID, + }) + if err != nil { + return GetSubscriptionAddonResponse{}, err + } + + return toAPISubscriptionAddon(view, *a) }, commonhttp.JSONResponseEncoderWithStatus[GetSubscriptionAddonResponse](http.StatusOK), httptransport.AppendOptions( diff --git a/api/v3/handlers/subscriptions/subscriptionaddons/handler.go b/api/v3/handlers/subscriptions/subscriptionaddons/handler.go index 5c21c685ee..120c861859 100644 --- a/api/v3/handlers/subscriptions/subscriptionaddons/handler.go +++ b/api/v3/handlers/subscriptions/subscriptionaddons/handler.go @@ -3,29 +3,38 @@ package subscriptionaddons import ( "context" + "github.com/openmeterio/openmeter/openmeter/subscription" subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" + subscriptionworkflow "github.com/openmeterio/openmeter/openmeter/subscription/workflow" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" ) type Handler interface { + CreateSubscriptionAddon() CreateSubscriptionAddonHandler ListSubscriptionAddons() ListSubscriptionAddonsHandler GetSubscriptionAddon() GetSubscriptionAddonHandler } type handler struct { - resolveNamespace func(ctx context.Context) (string, error) - addonService subscriptionaddon.Service - options []httptransport.HandlerOption + resolveNamespace func(ctx context.Context) (string, error) + addonService subscriptionaddon.Service + subscriptionService subscription.Service + subscriptionWorkflowService subscriptionworkflow.Service + options []httptransport.HandlerOption } func New( resolveNamespace func(ctx context.Context) (string, error), addonService subscriptionaddon.Service, + subscriptionService subscription.Service, + subscriptionWorkflowService subscriptionworkflow.Service, options ...httptransport.HandlerOption, ) Handler { return &handler{ - resolveNamespace: resolveNamespace, - addonService: addonService, - options: options, + resolveNamespace: resolveNamespace, + addonService: addonService, + subscriptionService: subscriptionService, + subscriptionWorkflowService: subscriptionWorkflowService, + options: options, } } diff --git a/api/v3/handlers/subscriptions/subscriptionaddons/list.go b/api/v3/handlers/subscriptions/subscriptionaddons/list.go index 95ce803666..7142eaa5d4 100644 --- a/api/v3/handlers/subscriptions/subscriptionaddons/list.go +++ b/api/v3/handlers/subscriptions/subscriptionaddons/list.go @@ -87,13 +87,24 @@ func (h *handler) ListSubscriptionAddons() ListSubscriptionAddonsHandler { return ListSubscriptionAddonsResponse{}, err } - items := make([]apiv3.SubscriptionAddon, 0, len(res.Items)) - for _, item := range res.Items { - converted, err := toAPISubscriptionAddon(item) - if err != nil { - return ListSubscriptionAddonsResponse{}, err - } - items = append(items, converted) + if len(res.Items) == 0 { + return response.NewPagePaginationResponse([]apiv3.SubscriptionAddon{}, response.PageMetaPage{ + Size: req.Input.Page.PageSize, + Number: req.Input.Page.PageNumber, + Total: lo.ToPtr(res.TotalCount), + }), nil + } + + view, err := h.subscriptionService.GetView(ctx, req.SubscriptionID) + if err != nil { + return ListSubscriptionAddonsResponse{}, err + } + + items, err := lo.MapErr(res.Items, func(item subscriptionaddon.SubscriptionAddon, _ int) (apiv3.SubscriptionAddon, error) { + return toAPISubscriptionAddon(view, item) + }) + if err != nil { + return ListSubscriptionAddonsResponse{}, err } return response.NewPagePaginationResponse(items, response.PageMetaPage{ diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 87a5c18ad3..1c4658b58c 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -2670,10 +2670,47 @@ paths: tags: - OpenMeter Subscriptions /openmeter/subscriptions/{subscriptionId}/addons: + post: + operationId: create-subscription-addon + summary: Create a new subscription add-on + description: Add add-on to a subscription. + parameters: + - name: subscriptionId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '201': + description: SubscriptionAddon created response. + content: + application/json: + schema: + $ref: '#/components/schemas/SubscriptionAddon' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + tags: + - OpenMeter Subscriptions + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubscriptionAddonRequest' + x-internal: true + x-unstable: true get: operationId: list-subscription-addons summary: List subscription addons - description: List the addons of a subscription. + description: List the add-ons of a subscription. parameters: - name: subscriptionId in: path @@ -7116,7 +7153,7 @@ components: behavior: $ref: '#/components/schemas/BillingTaxBehavior' code: - $ref: '#/components/schemas/TaxCodeReference' + $ref: '#/components/schemas/TaxCodeReferenceItem' additionalProperties: false description: The tax config of the rate card. BillingSettlementMode: @@ -8921,6 +8958,34 @@ components: $ref: '#/components/schemas/ULID' additionalProperties: false description: TaxCode reference. + CreateSubscriptionAddonRequest: + type: object + required: + - addon + - quantity + - timing + properties: + labels: + $ref: '#/components/schemas/Labels' + addon: + allOf: + - $ref: '#/components/schemas/AddonReference' + description: The add-on associated with the subscription. + title: Add-on + quantity: + type: integer + minimum: 1 + description: The quantity of the add-on. Always 1 for single instance add-ons. + title: Quantity + timing: + allOf: + - $ref: '#/components/schemas/BillingSubscriptionEditTiming' + description: |- + The timing of the operation. After the create or update, a new entry will be + created in the timeline. + title: Timing + additionalProperties: false + description: SubscriptionAddon create request. CreateTaxCodeRequest: type: object required: @@ -10791,33 +10856,20 @@ components: type: object required: - id - - name - created_at - updated_at + - name - addon - quantity - quantity_at - active_from + - timeline + - rate_cards properties: id: allOf: - $ref: '#/components/schemas/ULID' readOnly: true - name: - type: string - minLength: 1 - maxLength: 256 - description: |- - Display name of the resource. - - Between 1 and 256 characters. - description: - type: string - maxLength: 1024 - description: |- - Optional description of the resource. - - Maximum 1024 characters. labels: $ref: '#/components/schemas/Labels' created_at: @@ -10835,9 +10887,26 @@ components: - $ref: '#/components/schemas/DateTime' description: An ISO-8601 timestamp representation of entity deletion date. readOnly: true + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + readOnly: true + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + readOnly: true addon: allOf: - - $ref: '#/components/schemas/AddonReferenceItem' + - $ref: '#/components/schemas/AddonReference' description: The add-on associated with the subscription. title: Add-on quantity: @@ -10862,6 +10931,29 @@ components: - $ref: '#/components/schemas/DateTime' description: An ISO-8601 timestamp representation of the cadence end of the resource. readOnly: true + timeline: + type: array + items: + $ref: '#/components/schemas/SubscriptionAddonTimelineSegment' + description: The timeline of the add-on. The returned periods are sorted and continuous. + title: Timeline + example: + - quantity: 1 + active_from: '2025-01-01T00:00:00Z' + active_to: '2025-01-02T00:00:00Z' + - quantity: 0 + active_from: '2025-01-02T00:00:00Z' + active_to: '2025-01-03T00:00:00Z' + - quantity: 1 + active_from: '2025-01-03T00:00:00Z' + readOnly: true + rate_cards: + type: array + items: + $ref: '#/components/schemas/SubscriptionAddonRateCard' + description: The rate cards of the add-on. + title: Rate cards + readOnly: true additionalProperties: false description: Addon purchased with a subscription. SubscriptionAddonPagePaginatedResponse: @@ -10878,6 +10970,51 @@ components: $ref: '#/components/schemas/PaginatedMeta' additionalProperties: false description: Page paginated response. + SubscriptionAddonRateCard: + type: object + required: + - rate_card + - affected_subscription_item_ids + properties: + rate_card: + allOf: + - $ref: '#/components/schemas/BillingRateCard' + description: The rate card. + title: Rate card + affected_subscription_item_ids: + type: array + items: + $ref: '#/components/schemas/ULID' + description: The IDs of the subscription items that this rate card belongs to. + title: Affected subscription item IDs + readOnly: true + additionalProperties: false + description: A rate card for a subscription add-on. + SubscriptionAddonTimelineSegment: + type: object + required: + - active_from + - quantity + properties: + active_from: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of the cadence start of the resource. + readOnly: true + active_to: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of the cadence end of the resource. + readOnly: true + quantity: + type: integer + minimum: 0 + description: The quantity of the add-on for the given period. + title: Quantity + example: 1 + readOnly: true + additionalProperties: false + description: A subscription add-on event. SubscriptionPagePaginatedResponse: type: object required: @@ -10915,6 +11052,15 @@ components: $ref: '#/components/schemas/ULID' additionalProperties: false description: TaxCode reference. + TaxCodeReferenceItem: + type: object + required: + - id + properties: + id: + $ref: '#/components/schemas/ULID' + additionalProperties: false + description: TaxCode reference. ULID: type: string pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$ diff --git a/api/v3/server/routes.go b/api/v3/server/routes.go index 205dcffb8a..9abd68c935 100644 --- a/api/v3/server/routes.go +++ b/api/v3/server/routes.go @@ -121,6 +121,10 @@ func (s *Server) ChangeSubscription(w http.ResponseWriter, r *http.Request, subs } // Subscription Addons +func (s *Server) CreateSubscriptionAddon(w http.ResponseWriter, r *http.Request, subscriptionId api.ULID) { + s.subscriptionAddonsHandler.CreateSubscriptionAddon().With(subscriptionId).ServeHTTP(w, r) +} + func (s *Server) ListSubscriptionAddons(w http.ResponseWriter, r *http.Request, subscriptionId api.ULID, params api.ListSubscriptionAddonsParams) { s.subscriptionAddonsHandler.ListSubscriptionAddons().With(subscriptionaddonshandler.ListSubscriptionAddonsParams{ SubscriptionID: subscriptionId, diff --git a/api/v3/server/server.go b/api/v3/server/server.go index 61561a56e7..1b624ed3df 100644 --- a/api/v3/server/server.go +++ b/api/v3/server/server.go @@ -65,6 +65,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/streaming" "github.com/openmeterio/openmeter/openmeter/subscription" subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" + subscriptionworkflow "github.com/openmeterio/openmeter/openmeter/subscription/workflow" "github.com/openmeterio/openmeter/openmeter/taxcode" "github.com/openmeterio/openmeter/pkg/errorsx" "github.com/openmeterio/openmeter/pkg/featuregate" @@ -83,32 +84,33 @@ type Config struct { UnitConfig config.UnitConfigConfiguration // services - AddonService addon.Service - AppService app.Service - BillingService billing.Service - LLMCostService llmcost.Service - MeterService meter.ManageService - StreamingConnector streaming.Connector - IngestService ingest.Service - MeterEventService meterevent.Service - CustomerService customer.Service - CreditGrantService creditgrant.Service - Ledger ledger.Ledger - AccountResolver ledger.AccountResolver - CustomerBalanceFacade *customerbalance.Facade - EntitlementService entitlement.Service - GovernanceService governance.Service - PlanService plan.Service - PlanAddonService planaddon.Service - PlanSubscriptionService plansubscription.PlanSubscriptionService - StripeService appstripe.Service - SubscriptionService subscription.Service - SubscriptionAddonService subscriptionaddon.Service - TaxCodeService taxcode.Service - CurrencyService currencies.CurrencyService - ChargeService billingcharges.ChargeService - CostService cost.Service - FeatureConnector feature.FeatureConnector + AddonService addon.Service + AppService app.Service + BillingService billing.Service + LLMCostService llmcost.Service + MeterService meter.ManageService + StreamingConnector streaming.Connector + IngestService ingest.Service + MeterEventService meterevent.Service + CustomerService customer.Service + CreditGrantService creditgrant.Service + Ledger ledger.Ledger + AccountResolver ledger.AccountResolver + CustomerBalanceFacade *customerbalance.Facade + EntitlementService entitlement.Service + GovernanceService governance.Service + PlanService plan.Service + PlanAddonService planaddon.Service + PlanSubscriptionService plansubscription.PlanSubscriptionService + StripeService appstripe.Service + SubscriptionService subscription.Service + SubscriptionAddonService subscriptionaddon.Service + SubscriptionWorkflowService subscriptionworkflow.Service + TaxCodeService taxcode.Service + CurrencyService currencies.CurrencyService + ChargeService billingcharges.ChargeService + CostService cost.Service + FeatureConnector feature.FeatureConnector FeatureGate *featuregate.FeatureGateChecker } @@ -234,6 +236,10 @@ func (c *Config) Validate() error { errs = append(errs, err) } + if c.SubscriptionWorkflowService == nil { + errs = append(errs, errors.New("subscription workflow service is required")) + } + return errors.Join(errs...) } @@ -320,7 +326,7 @@ func NewServer(config *Config) (*Server, error) { customersEntitlementHandler := customersentitlementhandler.New(resolveNamespace, config.CustomerService, config.EntitlementService, httptransport.WithErrorHandler(config.ErrorHandler)) metersHandler := metershandler.New(resolveNamespace, config.MeterService, config.StreamingConnector, config.CustomerService, httptransport.WithErrorHandler(config.ErrorHandler)) subscriptionsHandler := subscriptionshandler.New(resolveNamespace, config.CustomerService, config.PlanService, config.PlanSubscriptionService, config.SubscriptionService, httptransport.WithErrorHandler(config.ErrorHandler)) - subscriptionAddonsHandler := subscriptionaddonshandler.New(resolveNamespace, config.SubscriptionAddonService, httptransport.WithErrorHandler(config.ErrorHandler)) + subscriptionAddonsHandler := subscriptionaddonshandler.New(resolveNamespace, config.SubscriptionAddonService, config.SubscriptionService, config.SubscriptionWorkflowService, httptransport.WithErrorHandler(config.ErrorHandler)) billingProfilesHandler := billingprofileshandler.New(resolveNamespace, config.BillingService, httptransport.WithErrorHandler(config.ErrorHandler)) billingInvoicesHandler := billinginvoiceshandler.New(resolveNamespace, config.BillingService, httptransport.WithErrorHandler(config.ErrorHandler)) plansHandler := planshandler.New(resolveNamespace, config.PlanService, config.UnitConfig.Enabled, httptransport.WithErrorHandler(config.ErrorHandler)) diff --git a/e2e/subscriptionaddons_v3_test.go b/e2e/subscriptionaddons_v3_test.go new file mode 100644 index 0000000000..1a95897f0f --- /dev/null +++ b/e2e/subscriptionaddons_v3_test.go @@ -0,0 +1,237 @@ +package e2e + +import ( + "net/http" + "testing" + "time" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + apiv3 "github.com/openmeterio/openmeter/api/v3" +) + +// TestV3SubscriptionAddonAttach exercises POST /subscriptions/{id}/addons end to end: +// build a published plan + published addon, create a subscription, attach the addon, +// verify the response shape (rate_cards/timeline arrays, never null), then confirm +// the conflict path returns 409 when the same addon is attached twice. +func TestV3SubscriptionAddonAttach(t *testing.T) { + c := newV3Client(t) + + // --- Fixture: customer --- + + customerKey := uniqueKey("sub_addon_customer") + custStatus, customer, custProblem := c.CreateCustomer(apiv3.CreateCustomerRequest{ + Key: customerKey, + Name: "Subscription Addon Test Customer", + Currency: lo.ToPtr(apiv3.CurrencyCode("USD")), + PrimaryEmail: lo.ToPtr("test-" + customerKey + "@test.com"), + UsageAttribution: &apiv3.BillingCustomerUsageAttribution{ + SubjectKeys: []string{customerKey}, + }, + }) + require.Equal(t, http.StatusCreated, custStatus, "problem: %+v", custProblem) + require.NotNil(t, customer) + + // --- Fixture: draft plan + published addon, attach addon, then publish plan --- + // Order matters: addons can only be attached to a plan while it is still in draft, + // and the addon must be published before attach. + + planBody := validPlanRequest("sub_addon_plan") + planStatus, plan, planProblem := c.CreatePlan(planBody) + require.Equal(t, http.StatusCreated, planStatus, "problem: %+v", planProblem) + require.NotNil(t, plan) + require.NotEmpty(t, plan.Phases, "plan must have at least one phase to attach an addon") + + addonBody := validAddonRequest("sub_addon") + addonStatus, addon, addonProblem := c.CreateAddon(addonBody) + require.Equal(t, http.StatusCreated, addonStatus, "problem: %+v", addonProblem) + require.NotNil(t, addon) + + pubAddonStatus, _, pubAddonProblem := c.PublishAddon(addon.Id) + require.Equal(t, http.StatusOK, pubAddonStatus, "problem: %+v", pubAddonProblem) + + attachStatus, _, attachProblem := c.AttachAddon(plan.Id, validPlanAddonRequest(plan.Phases[0].Key, addon.Id)) + require.Equal(t, http.StatusCreated, attachStatus, "problem: %+v", attachProblem) + + pubStatus, _, pubProblem := c.PublishPlan(plan.Id) + require.Equal(t, http.StatusOK, pubStatus, "problem: %+v", pubProblem) + + // --- Fixture: subscription on the published plan --- + + subBody := apiv3.BillingSubscriptionCreate{} + subBody.Customer.Id = &customer.Id + subBody.Plan.Id = &plan.Id + + subStatus, sub, subProblem := c.CreateSubscription(subBody) + require.Equal(t, http.StatusCreated, subStatus, "problem: %+v", subProblem) + require.NotNil(t, sub) + subscriptionID := sub.Id + + // --- Test: attach addon --- + + var subAddonID string + + t.Run("Should attach addon with immediate timing and return 201", func(t *testing.T) { + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("immediate"))) + + status, subAddon, problem := c.CreateSubscriptionAddon(subscriptionID, apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: addon.Id}, + Quantity: 1, + Timing: timing, + }) + require.Equal(t, http.StatusCreated, status, "problem: %+v", problem) + require.NotNil(t, subAddon) + + assert.NotEmpty(t, subAddon.Id) + assert.Equal(t, addon.Id, subAddon.Addon.Id) + assert.Equal(t, 1, subAddon.Quantity) + // Regression guard for the nil-slice → JSON null bug: rate_cards must be a non-nil array + // and every entry's affected_subscription_item_ids must be a non-nil array too. + assert.NotNil(t, subAddon.RateCards, "rate_cards must not be null") + for i, rc := range subAddon.RateCards { + assert.NotNil(t, rc.AffectedSubscriptionItemIds, "rate_cards[%d].affected_subscription_item_ids must not be null", i) + } + // Timeline must be a non-nil array with at least one segment for an active addon. + require.NotNil(t, subAddon.Timeline) + require.NotEmpty(t, subAddon.Timeline) + assert.Equal(t, 1, subAddon.Timeline[0].Quantity) + + subAddonID = subAddon.Id + }) + + t.Run("Should return 409 when attaching the same addon twice", func(t *testing.T) { + require.NotEmpty(t, subAddonID, "first attach must have succeeded") + + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("immediate"))) + + status, _, problem := c.CreateSubscriptionAddon(subscriptionID, apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: addon.Id}, + Quantity: 1, + Timing: timing, + }) + require.Equal(t, http.StatusConflict, status, "expected 409, got %d (problem: %+v)", status, problem) + }) + + t.Run("Should reject invalid quantity 0", func(t *testing.T) { + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("immediate"))) + + status, _, _ := c.CreateSubscriptionAddon(subscriptionID, apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: addon.Id}, + Quantity: 0, + Timing: timing, + }) + // TypeSpec @minValue(1) rejects this at schema-validation; workflow validation + // would also reject it. Either is fine — assert 4xx. + assert.GreaterOrEqual(t, status, http.StatusBadRequest, "expected 4xx for quantity=0, got %d", status) + assert.Less(t, status, http.StatusInternalServerError, "expected 4xx not 5xx for quantity=0") + }) + + t.Run("Should get the attached addon and surface rate_cards / timeline arrays", func(t *testing.T) { + require.NotEmpty(t, subAddonID) + + status, subAddon, problem := c.GetSubscriptionAddon(subscriptionID, subAddonID) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, subAddon) + + assert.Equal(t, subAddonID, subAddon.Id) + assert.NotNil(t, subAddon.RateCards, "GET: rate_cards must not be null") + assert.NotNil(t, subAddon.Timeline, "GET: timeline must not be null") + for i, rc := range subAddon.RateCards { + assert.NotNil(t, rc.AffectedSubscriptionItemIds, "GET: rate_cards[%d].affected_subscription_item_ids must not be null", i) + } + }) + + t.Run("Should list subscription addons and find the attached addon", func(t *testing.T) { + require.NotEmpty(t, subAddonID) + + status, page, problem := c.ListSubscriptionAddons(subscriptionID, withPageSize(100)) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, page) + + found := false + for _, sa := range page.Data { + if sa.Id == subAddonID { + found = true + assert.NotNil(t, sa.RateCards, "LIST: rate_cards must not be null") + assert.NotNil(t, sa.Timeline, "LIST: timeline must not be null") + break + } + } + assert.True(t, found, "attached subscription addon not found in list") + }) +} + +// TestV3SubscriptionAddonNextBillingCycle attaches an addon with timing=next_billing_cycle +// and verifies the create endpoint returns 201 (not the pre-fix 404). The new instance's +// active_from is in the future, so the no-current-instance fallback in toAPISubscriptionAddon +// must return quantity 0 instead of erroring out. +func TestV3SubscriptionAddonNextBillingCycle(t *testing.T) { + c := newV3Client(t) + + // --- Fixture: customer --- + + customerKey := uniqueKey("sub_addon_nbc_customer") + custStatus, customer, custProblem := c.CreateCustomer(apiv3.CreateCustomerRequest{ + Key: customerKey, + Name: "Next Billing Cycle Test Customer", + Currency: lo.ToPtr(apiv3.CurrencyCode("USD")), + PrimaryEmail: lo.ToPtr("test-" + customerKey + "@test.com"), + UsageAttribution: &apiv3.BillingCustomerUsageAttribution{ + SubjectKeys: []string{customerKey}, + }, + }) + require.Equal(t, http.StatusCreated, custStatus, "problem: %+v", custProblem) + + // --- Fixture: draft plan + published addon, attach, then publish plan --- + + planStatus, plan, planProblem := c.CreatePlan(validPlanRequest("sub_addon_nbc_plan")) + require.Equal(t, http.StatusCreated, planStatus, "problem: %+v", planProblem) + require.NotEmpty(t, plan.Phases) + + addonStatus, addon, addonProblem := c.CreateAddon(validAddonRequest("sub_addon_nbc")) + require.Equal(t, http.StatusCreated, addonStatus, "problem: %+v", addonProblem) + + pubAddonStatus, _, pubAddonProblem := c.PublishAddon(addon.Id) + require.Equal(t, http.StatusOK, pubAddonStatus, "problem: %+v", pubAddonProblem) + + attachStatus, _, attachProblem := c.AttachAddon(plan.Id, validPlanAddonRequest(plan.Phases[0].Key, addon.Id)) + require.Equal(t, http.StatusCreated, attachStatus, "problem: %+v", attachProblem) + + pubPlanStatus, _, pubPlanProblem := c.PublishPlan(plan.Id) + require.Equal(t, http.StatusOK, pubPlanStatus, "problem: %+v", pubPlanProblem) + + // Anchor the subscription at a past second so the next billing cycle is reliably + // in the future at the moment of the addon attach. + anchor := time.Now().Add(-time.Second) + subBody := apiv3.BillingSubscriptionCreate{BillingAnchor: &anchor} + subBody.Customer.Id = &customer.Id + subBody.Plan.Id = &plan.Id + + subStatus, sub, subProblem := c.CreateSubscription(subBody) + require.Equal(t, http.StatusCreated, subStatus, "problem: %+v", subProblem) + + t.Run("Should accept next_billing_cycle timing and return 201 with quantity 0", func(t *testing.T) { + var timing apiv3.BillingSubscriptionEditTiming + require.NoError(t, timing.FromBillingSubscriptionEditTimingEnum(apiv3.BillingSubscriptionEditTimingEnum("next_billing_cycle"))) + + status, subAddon, problem := c.CreateSubscriptionAddon(sub.Id, apiv3.CreateSubscriptionAddonRequest{ + Addon: apiv3.AddonReference{Id: addon.Id}, + Quantity: 1, + Timing: timing, + }) + require.Equal(t, http.StatusCreated, status, "problem: %+v", problem) + require.NotNil(t, subAddon) + + // Future-active addon: current quantity must be 0 (not an error). The timeline + // segment carries the requested quantity at its future activation point. + assert.Equal(t, 0, subAddon.Quantity, "current quantity must be 0 for future-active addon") + require.NotEmpty(t, subAddon.Timeline) + assert.Equal(t, 1, subAddon.Timeline[0].Quantity) + assert.True(t, subAddon.Timeline[0].ActiveFrom.After(time.Now()), "next_billing_cycle timing must produce a future active_from") + }) +} diff --git a/e2e/v3helpers_test.go b/e2e/v3helpers_test.go index 2b2f7ee3b8..e68ac652d4 100644 --- a/e2e/v3helpers_test.go +++ b/e2e/v3helpers_test.go @@ -370,6 +370,23 @@ func (c *v3Client) GetCustomerCreditBalance(customerID string) (int, *apiv3.Bill return decodeTyped[apiv3.BillingCreditBalances](c, status, raw, problem, http.StatusOK) } +// --- Subscription addons --- + +func (c *v3Client) CreateSubscriptionAddon(subscriptionID string, body apiv3.CreateSubscriptionAddonRequest) (int, *apiv3.SubscriptionAddon, *v3Problem) { + status, raw, problem := c.do(http.MethodPost, "/subscriptions/"+subscriptionID+"/addons", body) + return decodeTyped[apiv3.SubscriptionAddon](c, status, raw, problem, http.StatusCreated) +} + +func (c *v3Client) GetSubscriptionAddon(subscriptionID, subscriptionAddonID string) (int, *apiv3.SubscriptionAddon, *v3Problem) { + status, raw, problem := c.do(http.MethodGet, "/subscriptions/"+subscriptionID+"/addons/"+subscriptionAddonID, nil) + return decodeTyped[apiv3.SubscriptionAddon](c, status, raw, problem, http.StatusOK) +} + +func (c *v3Client) ListSubscriptionAddons(subscriptionID string, opts ...listOption) (int, *apiv3.SubscriptionAddonPagePaginatedResponse, *v3Problem) { + status, raw, problem := c.do(http.MethodGet, "/subscriptions/"+subscriptionID+"/addons"+buildPageQuery(opts), nil) + return decodeTyped[apiv3.SubscriptionAddonPagePaginatedResponse](c, status, raw, problem, http.StatusOK) +} + // --- List pagination options --- // listOptions controls pagination query params for list endpoints. The server diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 24379e6827..479363b20a 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -4866,6 +4866,14 @@ var ( Unique: false, Columns: []*schema.Column{SubscriptionAddonsColumns[1]}, }, + { + Name: "subscriptionaddon_namespace_subscription_id_addon_id", + Unique: true, + Columns: []*schema.Column{SubscriptionAddonsColumns[1], SubscriptionAddonsColumns[7], SubscriptionAddonsColumns[6]}, + Annotation: &entsql.IndexAnnotation{ + Where: "deleted_at IS NULL", + }, + }, }, } // SubscriptionAddonQuantitiesColumns holds the columns for the "subscription_addon_quantities" table. diff --git a/openmeter/ent/schema/subscription_addon.go b/openmeter/ent/schema/subscription_addon.go index c9a7eebbdd..952fa68464 100644 --- a/openmeter/ent/schema/subscription_addon.go +++ b/openmeter/ent/schema/subscription_addon.go @@ -32,6 +32,18 @@ func (SubscriptionAddon) Fields() []ent.Field { } } +// Indexes guarantees a subscription can hold at most one live (non-deleted) row +// per addon, which is the conflict contract surfaced by the v3 create endpoint. +func (SubscriptionAddon) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("namespace", "subscription_id", "addon_id"). + Annotations( + entsql.IndexWhere("deleted_at IS NULL"), + ). + Unique(), + } +} + func (SubscriptionAddon) Edges() []ent.Edge { return []ent.Edge{ edge.From("subscription", Subscription.Type). diff --git a/openmeter/server/server.go b/openmeter/server/server.go index d1cdfc9325..4b0c06a28f 100644 --- a/openmeter/server/server.go +++ b/openmeter/server/server.go @@ -135,41 +135,42 @@ func NewServer(config *Config) (*Server, error) { }...) v3API, err := v3server.NewServer(&v3server.Config{ - BaseURL: "/api/v3", - NamespaceDecoder: config.RouterConfig.NamespaceDecoder, - ErrorHandler: config.RouterConfig.ErrorHandler, - Credits: config.RouterConfig.Credits, - UnitConfig: config.RouterConfig.UnitConfig, - AddonService: config.RouterConfig.Addon, - AppService: config.RouterConfig.App, - BillingService: config.RouterConfig.Billing, - CustomerService: config.RouterConfig.Customer, - CreditGrantService: config.RouterConfig.CreditGrantService, - Ledger: config.RouterConfig.Ledger, - AccountResolver: config.RouterConfig.AccountResolver, - CustomerBalanceFacade: config.RouterConfig.CustomerBalanceFacade, - CurrencyService: config.RouterConfig.CurrencyService, - EntitlementService: config.RouterConfig.EntitlementConnector, - GovernanceService: config.RouterConfig.GovernanceService, - IngestService: config.RouterConfig.IngestService, - MeterEventService: config.RouterConfig.MeterEventService, - LLMCostService: config.RouterConfig.LLMCostService, - MeterService: config.RouterConfig.MeterManageService, - StreamingConnector: config.RouterConfig.StreamingConnector, - PlanService: config.RouterConfig.Plan, - PlanAddonService: config.RouterConfig.PlanAddon, - PlanSubscriptionService: config.RouterConfig.PlanSubscriptionService, - StripeService: config.RouterConfig.AppStripe, - SubscriptionService: config.RouterConfig.SubscriptionService, - SubscriptionAddonService: config.RouterConfig.SubscriptionAddonService, - ChargeService: config.RouterConfig.ChargeService, - TaxCodeService: config.RouterConfig.TaxCodeService, - CostService: config.RouterConfig.CostService, - FeatureConnector: config.RouterConfig.FeatureConnector, - Middlewares: v3Middlewares, - PostAuthMiddlewares: config.PostAuthMiddlewares, - ResponseValidation: config.ResponseValidation, - FeatureGate: config.RouterConfig.FeatureGate, + BaseURL: "/api/v3", + NamespaceDecoder: config.RouterConfig.NamespaceDecoder, + ErrorHandler: config.RouterConfig.ErrorHandler, + Credits: config.RouterConfig.Credits, + UnitConfig: config.RouterConfig.UnitConfig, + AddonService: config.RouterConfig.Addon, + AppService: config.RouterConfig.App, + BillingService: config.RouterConfig.Billing, + CustomerService: config.RouterConfig.Customer, + CreditGrantService: config.RouterConfig.CreditGrantService, + Ledger: config.RouterConfig.Ledger, + AccountResolver: config.RouterConfig.AccountResolver, + CustomerBalanceFacade: config.RouterConfig.CustomerBalanceFacade, + CurrencyService: config.RouterConfig.CurrencyService, + EntitlementService: config.RouterConfig.EntitlementConnector, + GovernanceService: config.RouterConfig.GovernanceService, + IngestService: config.RouterConfig.IngestService, + MeterEventService: config.RouterConfig.MeterEventService, + LLMCostService: config.RouterConfig.LLMCostService, + MeterService: config.RouterConfig.MeterManageService, + StreamingConnector: config.RouterConfig.StreamingConnector, + PlanService: config.RouterConfig.Plan, + PlanAddonService: config.RouterConfig.PlanAddon, + PlanSubscriptionService: config.RouterConfig.PlanSubscriptionService, + StripeService: config.RouterConfig.AppStripe, + SubscriptionService: config.RouterConfig.SubscriptionService, + SubscriptionAddonService: config.RouterConfig.SubscriptionAddonService, + SubscriptionWorkflowService: config.RouterConfig.SubscriptionWorkflowService, + ChargeService: config.RouterConfig.ChargeService, + TaxCodeService: config.RouterConfig.TaxCodeService, + CostService: config.RouterConfig.CostService, + FeatureConnector: config.RouterConfig.FeatureConnector, + Middlewares: v3Middlewares, + PostAuthMiddlewares: config.PostAuthMiddlewares, + ResponseValidation: config.ResponseValidation, + FeatureGate: config.RouterConfig.FeatureGate, }) if err != nil { return nil, fmt.Errorf("failed to create v3 API: %w", err) diff --git a/openmeter/subscription/addon/repo/subscriptionaddon.go b/openmeter/subscription/addon/repo/subscriptionaddon.go index 10139db631..0373813c7d 100644 --- a/openmeter/subscription/addon/repo/subscriptionaddon.go +++ b/openmeter/subscription/addon/repo/subscriptionaddon.go @@ -2,8 +2,11 @@ package subscriptionaddonrepo import ( "context" + "errors" "fmt" + "github.com/jackc/pgx/v5/pgconn" + "github.com/openmeterio/openmeter/openmeter/ent/db" dbsubscriptionaddon "github.com/openmeterio/openmeter/openmeter/ent/db/subscriptionaddon" dbsubscriptionaddonquantity "github.com/openmeterio/openmeter/openmeter/ent/db/subscriptionaddonquantity" @@ -40,6 +43,19 @@ func (r *subscriptionAddonRepo) Create(ctx context.Context, namespace string, in entity, err := cmd.Save(ctx) if err != nil { + // Surface the partial unique index on (namespace, subscription_id, addon_id) + // as a conflict so concurrent creators see the same error as the in-tx duplicate check. + // Keep the user-visible message generic so it does not echo addon/subscription IDs; + // the workflow's pre-Create validation already establishes that both exist. + if db.IsConstraintError(err) { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return nil, models.NewGenericConflictError( + fmt.Errorf("addon is already attached to subscription"), + ) + } + } + return nil, err } diff --git a/tools/migrate/migrations/20260629085650_add_subscription_addon_unique_index.down.sql b/tools/migrate/migrations/20260629085650_add_subscription_addon_unique_index.down.sql new file mode 100644 index 0000000000..c093d1c405 --- /dev/null +++ b/tools/migrate/migrations/20260629085650_add_subscription_addon_unique_index.down.sql @@ -0,0 +1,2 @@ +-- reverse: create index "subscriptionaddon_namespace_subscription_id_addon_id" to table: "subscription_addons" +DROP INDEX "subscriptionaddon_namespace_subscription_id_addon_id"; diff --git a/tools/migrate/migrations/20260629085650_add_subscription_addon_unique_index.up.sql b/tools/migrate/migrations/20260629085650_add_subscription_addon_unique_index.up.sql new file mode 100644 index 0000000000..3bad2b3add --- /dev/null +++ b/tools/migrate/migrations/20260629085650_add_subscription_addon_unique_index.up.sql @@ -0,0 +1,3 @@ +-- create index "subscriptionaddon_namespace_subscription_id_addon_id" to table: "subscription_addons" +-- atlas:nolint MF101 +CREATE UNIQUE INDEX "subscriptionaddon_namespace_subscription_id_addon_id" ON "subscription_addons" ("namespace", "subscription_id", "addon_id") WHERE (deleted_at IS NULL); diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index c29ab461e2..efaabb67a3 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:fVjzHgerEpm1WalMpLe3OYgss0yr4JmrC6kI+m5+a1U= +h1:iVCbRqh+JFjpkF6t9+7is+LixN+iV1lrZWtH+iLFiyI= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -215,3 +215,4 @@ h1:fVjzHgerEpm1WalMpLe3OYgss0yr4JmrC6kI+m5+a1U= 20260624043300_charge_intent_override_ddl.up.sql h1:IQckC6jS1yadEgZqiGkOgn51lhdwTt7d2IVDsIWipts= 20260624135146_remove_deprecated_ent_fields.up.sql h1:x5paHYKtNbRUBkY9UEHdc+fCkdIxIMt3tqfkMDEtfUM= 20260625202530_add_unit_config.up.sql h1:Ta8PhtlFboqQfTpJ6zhK4HDyivXu5bVAyV75YUxi9QE= +20260629085650_add_subscription_addon_unique_index.up.sql h1:nroCbZHWjWs4jOxcFvHAIOOV6QJFVrnJ+AO8bTB+N+M=