1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 14:34:02 +00:00

Working for org creation

This commit is contained in:
Jonas Hendrickx
2024-11-21 19:22:00 +01:00
parent f1368cd0e6
commit 6c283639e7
5 changed files with 127 additions and 31 deletions

View File

@@ -335,7 +335,7 @@
>{{ "additionalUsers" | i18n }}:</span
>
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ formGroup.controls.additionalSeats.value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12
@@ -355,7 +355,7 @@
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
>
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ formGroup.controls.additionalStorage.value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
@@ -388,7 +388,7 @@
>{{ "additionalUsers" | i18n }}:</span
>
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ formGroup.controls.additionalSeats.value || 0 }} &times;
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{
@@ -403,7 +403,7 @@
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
>
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ formGroup.controls.additionalStorage.value || 0 }} &times;
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
@@ -440,7 +440,10 @@
<app-payment-v2
*ngIf="deprecateStripeSourcesAPI && (createOrganization || upgradeRequiresPaymentMethod)"
></app-payment-v2>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<app-tax-info
(onCountryChanged)="changedCountry()"
(onTaxInformationChanged)="onTaxInformationChanged()"
/>
<div id="price" class="tw-my-4">
<div class="tw-text-muted tw-text-base">
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
@@ -450,7 +453,7 @@
<br />
</span>
<ng-container>
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
</ng-container>
</div>
<hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />

View File

@@ -10,6 +10,7 @@ import {
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
@@ -24,9 +25,11 @@ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/mode
import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request";
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
@@ -72,11 +75,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
@ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string;
@Input() organizationId?: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
@Input() currentPlan: PlanResponse;
selectedFile: File;
@Input()
@@ -147,7 +151,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
billing: BillingResponse;
provider: ProviderResponse;
private destroy$ = new Subject<void>();
protected estimatedTax: number;
protected total: number;
private destroy$: Subject<void> = new Subject<void>();
constructor(
private apiService: ApiService,
@@ -166,6 +173,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private toastService: ToastService,
private configService: ConfigService,
private billingApiService: BillingApiServiceAbstraction,
private taxService: TaxServiceAbstraction,
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
@@ -239,6 +247,16 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
this.loading = false;
this.formGroup.valueChanges.pipe(debounceTime(1000), takeUntil(this.destroy$)).subscribe(() => {
this.refreshSalesTax();
});
this.secretsManagerForm.valueChanges
.pipe(debounceTime(1000), takeUntil(this.destroy$))
.subscribe(() => {
this.refreshSalesTax();
});
}
ngOnDestroy() {
@@ -436,17 +454,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
return this.selectedPlan.trialPeriodDays != null;
}
get taxCharges() {
return this.taxComponent != null && this.taxComponent.taxRate != null
? (this.taxComponent.taxRate / 100) *
(this.passwordManagerSubtotal + this.secretsManagerSubtotal)
: 0;
}
get total() {
return this.passwordManagerSubtotal + this.secretsManagerSubtotal + this.taxCharges || 0;
}
get paymentDesc() {
if (this.acceptingSponsorship) {
return this.i18nService.t("paymentSponsored");
@@ -552,7 +559,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.changedProduct();
}
changedCountry() {
protected changedCountry(): void {
if (this.deprecateStripeSourcesAPI) {
this.paymentV2Component.showBankAccount = this.taxComponent.country === "US";
if (
@@ -573,11 +580,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
}
cancel() {
protected onTaxInformationChanged(): void {
this.refreshSalesTax();
}
protected cancel(): void {
this.onCanceled.emit();
}
setSelectedFile(event: Event) {
protected setSelectedFile(event: Event): void {
const fileInputEl = <HTMLInputElement>event.target;
this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
}
@@ -594,7 +605,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
return;
}
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
let orgId: string;
if (this.createOrganization) {
const orgKey = await this.keyService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
@@ -605,11 +616,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
const collectionCt = collection.encryptedString;
const orgKeys = await this.keyService.makeKeyPair(orgKey[1]);
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
}
orgId = this.selfHosted
? await this.createSelfHosted(key, collectionCt, orgKeys)
: await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
this.toastService.showToast({
variant: "success",
@@ -617,7 +626,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
message: this.i18nService.t("organizationReadyToGo"),
});
} else {
orgId = await this.updateOrganization(orgId);
orgId = await this.updateOrganization();
this.toastService.showToast({
variant: "success",
title: null,
@@ -651,7 +660,47 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.messagingService.send("organizationCreated", { organizationId });
};
private async updateOrganization(orgId: string) {
private refreshSalesTax(): void {
if (!this.taxComponent.country || !this.taxComponent.postalCode) {
return;
}
const request: PreviewOrganizationInvoiceRequest = {
organizationId: this.organizationId,
passwordManager: {
additionalStorage: this.formGroup.controls.additionalStorage.value,
plan: this.formGroup.controls.plan.value,
seats: this.formGroup.controls.additionalSeats.value,
},
taxInformation: {
postalCode: this.taxComponent.postalCode,
country: this.taxComponent.country,
taxId: this.taxComponent.taxId,
},
};
if (this.secretsManagerForm.controls.enabled.value === true) {
request.secretsManager = {
seats: this.secretsManagerForm.controls.userSeats.value,
additionalMachineAccounts: this.secretsManagerForm.controls.additionalServiceAccounts.value,
};
}
this.taxService
.previewOrganizationInvoice(request)
.then((invoice) => {
this.estimatedTax = invoice.taxAmount;
this.total = invoice.totalAmount;
})
.catch((error) => {
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
});
});
}
private async updateOrganization() {
const request = new OrganizationUpgradeRequest();
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
@@ -707,7 +756,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
collectionCt: string,
orgKeys: [string, EncString],
orgKey: SymmetricCryptoKey,
) {
): Promise<string> {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;

View File

@@ -1,5 +1,6 @@
import { CountryListItem } from "@bitwarden/common/billing/models/domain";
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response";
export abstract class TaxServiceAbstraction {
@@ -10,4 +11,8 @@ export abstract class TaxServiceAbstraction {
previewIndividualInvoice: (
request: PreviewIndividualInvoiceRequest,
) => Promise<PreviewInvoiceResponse>;
previewOrganizationInvoice: (
request: PreviewOrganizationInvoiceRequest,
) => Promise<PreviewInvoiceResponse>;
}

View File

@@ -0,0 +1,25 @@
import { PlanType } from "@bitwarden/common/billing/enums";
export class PreviewOrganizationInvoiceRequest {
organizationId?: string;
passwordManager: PasswordManager;
secretsManager?: SecretsManager;
taxInformation: TaxInformation;
}
class PasswordManager {
plan: PlanType;
seats: number;
additionalStorage: number;
}
class SecretsManager {
seats: number;
additionalMachineAccounts: number;
}
class TaxInformation {
postalCode: string;
country: string;
taxId: string;
}

View File

@@ -2,6 +2,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { CountryListItem } from "@bitwarden/common/billing/models/domain";
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response";
export class TaxService implements TaxServiceAbstraction {
@@ -286,4 +287,17 @@ export class TaxService implements TaxServiceAbstraction {
);
return new PreviewInvoiceResponse(response);
}
async previewOrganizationInvoice(
request: PreviewOrganizationInvoiceRequest,
): Promise<PreviewInvoiceResponse> {
const response = await this.apiService.send(
"POST",
`/invoices/preview-organization`,
request,
true,
true,
);
return new PreviewInvoiceResponse(response);
}
}