Search code examples
angular6paypal-sandbox

Angular 6 PayPal VALIDATION_ERROR


I am trying to use [PayPal's JavaScript code with their www.sandbox.paypal.com website1 in Angular 6 and I am receiving the following error:

Error: Request to post https://www.sandbox.paypal.com/v1/payments/payment failed with 400 error. Correlation id: 6c0ce47611849, 6c0ce47611849

{
    "name": "VALIDATION_ERROR",
    "details": [
        {
            "field": "transactions.amount",
            "issue": "Currency amount must be non-negative number, may optionally contain exactly 2 decimal places separated by '.', optional thousands separator ',', limited to 7 digits before the decimal point and currency which is a valid ISO Currency Code"
        }
    ],
    "message": "Invalid request - see details",
    "information_link": "https://developer.paypal.com/docs/api/payments/#errors",
    "debug_id": "6c0ce47611849"
}

request/</<@https://www.paypalobjects.com/api/checkout.js:14216:39

As you can see with the following screenshot, the format looks correct to me: the format looks correct to me[2]

The following is my shipping form html:

 <div class="container">
  <div class="row">
    <div class="col-md-8 order-md-1">
      <h4 class="mb-3">Shipping address</h4>
      <form name="form" #f="ngForm" novalidate>
        <div class="row">
          <div class="col-md-6 mb-3">
            <label for="firstName">First name</label>
            <input
              type="text"
              class="form-control"
              [(ngModel)]="model.firstName"
              #firstName="ngModel"
              [ngClass]="{
                'is-invalid': f.submitted && firstName.invalid
              }"
              name="firstName"
              placeholder="First name"
              required
            />
            <div
              *ngIf="f.submitted && firstName.required"
              class="invalid-feedback"
            >
              <div *ngIf="firstName.errors.required">
                First name is required
              </div>
            </div>
          </div>
          <div class="col-md-6 mb-3">
            <label for="lastName">Last name</label>
            <input
              type="text"
              class="form-control"
              [(ngModel)]="model.lastName"
              #lastName="ngModel"
              [ngClass]="{
                'is-invalid': f.submitted && lastName.invalid
              }"
              name="lastName"
              placeholder="Last name"
              required
            />
            <div
              *ngIf="f.submitted && lastName.required"
              class="invalid-feedback"
            >
              <div *ngIf="lastName.errors.required">Last name is required</div>
            </div>
          </div>
        </div>

        <div class="mb-3">
          <label for="email"
            >Email <span class="text-muted">(Same as username)</span></label
          >
          <input
            type="email"
            class="form-control"
            [(ngModel)]="model.email"
            #email="ngModel"
            [ngClass]="{
              'is-invalid': f.submitted && email.invalid
            }"
            name="email"
            placeholder="you@example.com"
            required
            value="{{ model.email }}"
            email
          />
          <div *ngIf="f.submitted && email.required" class="invalid-feedback">
            <div *ngIf="email.errors.email">Email is required</div>
          </div>
        </div>

        <div class="mb-3">
          <label for="address">Address</label>
          <input
            type="text"
            class="form-control"
            [(ngModel)]="model.address"
            #address="ngModel"
            [ngClass]="{
              'is-invalid': f.submitted && address.invalid
            }"
            name="address"
            placeholder="1234 Main St"
            required
          />
          <div *ngIf="f.submitted && address.required" class="invalid-feedback">
            <div *ngIf="address.errors.required">
              Shipping address is required
            </div>
          </div>
        </div>

        <div class="mb-3">
          <label for="address2"
            >Address 2 <span class="text-muted">(Optional)</span></label
          >
          <input
            type="text"
            class="form-control"
            [(ngModel)]="model.address2"
            name="address2"
            placeholder="Apartment or suite"
            #address2="ngModel"
          />
        </div>

        <div class="mb-3">
          <label for="city">City <span class="text-muted"></span></label>
          <input
            type="text"
            class="form-control"
            [(ngModel)]="model.city"
            name="city"
            placeholder="Apartment or suite"
            #city="ngModel"
          />
        </div>

        <div class="mb-3">
          <label for="phone">Phone <span class="text-muted"></span></label>
          <input
            type="text"
            class="form-control"
            [(ngModel)]="model.phone"
            name="phone"
            placeholder="Apartment or suite"
            #phone="ngModel"
          />
        </div>

        <div class="row">
          <div class="col-md-5 mb-3">
            <label for="country">Country</label>
            <select
              class="custom-select d-block w-100"
              [(ngModel)]="model.country"
              [ngClass]="{
                'is-invalid': f.submitted && country.invalid
              }"
              name="country"
              #country="ngModel"
              required
            >
              <option value="" selected="selected"
                >Please choose a country</option
              >
              <option
                *ngFor="let country of countriesObj"
                value="{{ country.code }}"
                >{{ country.name }}</option
              >
            </select>
            <div
              *ngIf="f.submitted && country.required"
              class="invalid-feedback"
            >
              <div *ngIf="country.errors.required">Country is required</div>
            </div>
          </div>
          <div class="mb-3">
            <label for="state">State/Province</label>
            <input
              type="text"
              class="form-control"
              [(ngModel)]="model.state"
              [ngClass]="{
                'is-invalid': f.submitted && state.invalid
              }"
              name="state"
              placeholder="California"
              #state="ngModel"
              required
            />
            <div *ngIf="f.submitted && state.required" class="invalid-feedback">
              <div *ngIf="state.errors.required">
                State/Province is required
              </div>
            </div>
          </div>
          <div class="col-md-3 mb-3">
            <label for="zip">Zip/Postal Code</label>
            <input
              type="text"
              class="form-control"
              [(ngModel)]="model.zip"
              [ngClass]="{
                'is-invalid': f.submitted && zip.invalid
              }"
              name="zip"
              placeholder="Zip/Postal code"
              #zip="ngModel"
              required
            />
            <div *ngIf="f.submitted && zip.required" class="invalid-feedback">
              <div *ngIf="zip.errors.required">Zip/Postal code is required</div>
            </div>
          </div>
        </div>
        <hr class="mb-4" />

        <h4 class="mb-3">Payment</h4>
        <div id="paypal-button-container"></div>
        <input
          type="text"
          [(ngModel)]="model.total"
          style="padding-bottom: 10px;"
          name="total"
          #total="ngModel"
          value="{{ model.total | currency }}"
        />
        <h2 *ngIf="paypalLoad">Paypal button is loading</h2>
        <div id="paypal-checkout-btn"></div>
      </form>
    </div>
  </div>
</div>
<div class="ModalBackdrop"></div>
Final amount: {{ this.finalAmount }}
<!-- The Modal -->
<div class="modal" id="myModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <!-- Modal Header -->
      <div class="modal-header">
        <h4 class="modal-title">Thank you</h4>
        <button type="button" class="close" data-dismiss="modal">
          &times;
        </button>
      </div>

      <!-- Modal body -->
      <div class="modal-body">
        This is confirmation that your payment has been processed. Your order
        will shipped as soon as possible.
      </div>

      <!-- Modal footer -->
      <div class="modal-footer">
        <button type="button" class="btn btn-danger" data-dismiss="modal" onclick="document.getElementById('ModalBackdrop').style.display = 'none';">
          Close
        </button>
      </div>
    </div>
  </div>
</div>

The following is my shipping form component - as you can see, I have tried a few different things:

import { Component, OnInit, AfterViewChecked } from "@angular/core";
import { HttpClient, HttpHeaders, HttpResponse } from "@angular/common/http";
import { ShoppingCartService } from "../shopping-cart.service";
import { ProductService } from "../product.service";
import { LoginService } from "../login.service";
import { modelGroupProvider } from "@angular/forms/src/directives/ng_model_group";

declare let paypal: any;

@Component({
  selector: "app-shipping-form",
  templateUrl: "./shipping-form.component.html",
  styleUrls: ["./shipping-form.component.css"]
})
export class ShippingFormComponent implements OnInit, AfterViewChecked {
  public countriesObj;
  total;
  public model: any = {};
  firstName = "";
  lastName = "";
  email = this.loginSrvc.loginObj.uName;
  address = "";
  address2 = "";
  city = "";
  phone = "";
  country = "";
  state = "";
  zip = "";
  name = "";
  shoppingCartObj: any;
  user_id = "";
  orders_id = "";
  product = "";
  ts = "";

  constructor(
    private http: HttpClient,
    private loginSrvc: LoginService,
    private prdSrvc: ProductService,
    private cartSrvc: ShoppingCartService
  ) {
    this.http.get("./assets/countries.json").subscribe(result => {
      this.countriesObj = result;
    });
    this.total = this.prdSrvc.getPriceTotal();
    this.model.total = this.total.toFixed(2);
    this.model.email = this.loginSrvc.loginObj.uName;
    // this.model.firstName = this.firstName;
    // this.model.lastName = this.lastName;
    this.name = this.model.firstName + " " + this.model.lastName;
    this.email = this.model.email;
    this.address = this.model.address;
    this.address2 = this.model.address2;
    this.city = this.model.city;
    this.state = this.model.state;
    this.country = this.model.country;
    this.zip = this.model.zip;
    this.shoppingCartObj = JSON.parse(localStorage.getItem("fh_cart"));
    this.user_id = this.shoppingCartObj.cart.user_id;
    this.orders_id = this.shoppingCartObj.cart.orders_id;
    this.product = JSON.stringify(this.shoppingCartObj.cart.products.product);
    this.ts = this.shoppingCartObj.cart.ts;
  }

  addScript = false;
  paypalLoad = true;
  finalAmount: number = this.getFinalAmount();

  paypalConfig = {
    env: "sandbox",
    client: {
      sandbox:
        "<my-sandbox-key>",
      production: "<your-production-key here>"
    },
    commit: true,
    payment: function(data, actions) {
      return actions.payment.create({
        transactions: [
          {
            amount: {
              total: this.finalAmount,
              currency: "USD",
              details: {
                subtotal: "30.00",
                tax: "0.07",
                shipping: "0.03",
                handling_fee: "1.00",
                shipping_discount: "-1.00",
                insurance: "0.01"
              }
            },
            description: "The payment from for-her application.",
            /* custom: "90048630024435", */
            invoice_number: this.orders_id, // Insert a unique invoice number
            payment_options: {
              allowed_payment_method: "INSTANT_FUNDING_SOURCE"
            },
            soft_descriptor: this.user_id,
            item_list: {
              items: [this.product],
              shipping_address: {
                recipient_name: this.name,
                line1: this.address,
                line2: this.address2,
                city: this.city,
                country_code: this.country,
                postal_code: this.zip,
                phone: this.phone,
                state: this.state,
                email: this.email
              }
            }
          }
        ],
        note_to_payer: "Contact us for any questions on your order."
      });
    },
    onAuthorize: (data, actions) => {
      return actions.payment.execute().then(payment => {
        // Do something when payment is successful.
        // window.alert("Thank you for your purchase! You order will be processed and shipped as soon as possible");
        document.getElementById("myModal").style.display = "block";
        document.getElementById("ModalBackdrop").style.display = "block";
        this.cartSrvc.postCart();
      });
    }
  };

  ngAfterViewChecked(): void {
    if (!this.addScript) {
      this.addPaypalScript().then(() => {
        paypal.Button.render(this.paypalConfig, "#paypal-checkout-btn");
        this.paypalLoad = false;
      });
    }
  }

  addPaypalScript() {
    this.addScript = true;
    return new Promise((resolve, reject) => {
      const scripttagElement = document.createElement("script");
      scripttagElement.src = "https://www.paypalobjects.com/api/checkout.js";
      scripttagElement.onload = resolve;
      document.body.appendChild(scripttagElement);
    });
  }
  getFinalAmount() {
    this.total = this.prdSrvc.getPriceTotal();
    this.model.total = this.total.toFixed(2);
    return this.total.toFixed(2);
  }

  ngOnInit() {}
}

As you can see, I followed their documentation, with the exception of using dynamic values. If I hard code in the values, as their example shows, then I have no errors. Why would it consider finalAmount as not valid?

Thanks to @paulsm4 in the comments, the network tab is not showing in values, except for the hard coded values:

hard coded values only coming through

As usual, thanks in advance


Solution

  • I do not know if this is the proper way to do this, but I am no longer receiving the original error.

    First, I created several hidden fields to the corresponding items that I wanted to be posted, along with giving every input an id - the following are the hidden fields:

        <input
          type="text"
          [(ngModel)]="model.total"
          style="padding-bottom: 10px;"
          name="total"
          id="total"
          #total="ngModel"
          value="{{ model.total | currency }}"
        />
        <input
          type="hidden"
          name="user_id"
          id="user_id"
          value="{{ this.user_id }}"
        />
        <input
          type="hidden"
          name="orders_id"
          id="orders_id"
          value="{{ this.orders_id }}"
        />
        <input
          type="hidden"
          name="product"
          id="product"
          value="{{ this.product }}"
        />
        <input
          type="hidden"
          name="subTotal"
          id="subTotal"
          value="{{ this.payPalSrvc.getSubTotal() }}"
        />
    

    Second, I created a separate PayPalService, to separate functionality, along with, I wanted these posts and gets to go through my interceptor to place jwt in the headers:

    import { Injectable } from "@angular/core";
    import { HttpClient, HttpHeaders, HttpResponse } from "@angular/common/http";
    import { ShoppingCartService } from "./shopping-cart.service";
    import { ProductService } from "./product.service";
    import { LoginService } from "./login.service";
    
    @Injectable({
      providedIn: "root"
    })
    export class PaypalService {
      constructor(
        private http: HttpClient,
        private loginSrvc: LoginService,
        private prdSrvc: ProductService,
        private cartSrvc: ShoppingCartService
      ) {}
    
      addScript = false;
      paypalLoad = true;
      finalAmount;
      subTotal;
    
      paypalConfig = {
        env: "sandbox",
        client: {
          sandbox:
            "<my-sandbox-id>",
          production: "<your-production-key here>"
        },
        commit: true,
        payment: function(data, actions) {
          return actions.payment.create({
            transactions: [
              {
                amount: {
                  total: document.getElementById("total").value,
                  currency: "USD",
                  details: {
                    subtotal: document.getElementById("subTotal").value,
                    tax: (document.getElementById("total").value * 0.07).toFixed(2),
                    shipping: (document.getElementById("total").value * 0.03).toFixed(2),
                    handling_fee: "1.00",
                    shipping_discount: "0.00",
                    insurance: (document.getElementById("total").value * 0.01).toFixed(2)
                  }
                },
                description: "The payment from for-her application.",
                /* custom: "90048630024435", */
                invoice_number: document.getElementById("orders_id").value, // Insert a unique invoice number
                payment_options: {
                  allowed_payment_method: "INSTANT_FUNDING_SOURCE"
                },
                soft_descriptor: document.getElementById("user_id").value,
                item_list: {
                  items: [document.getElementById("product").value],
                  shipping_address: {
                    recipient_name: (document.getElementById("firstName").value + " " + document.getElementById("lastName").value),
                    line1: document.getElementById("address").value,
                    line2: document.getElementById("address2").value,
                    city: document.getElementById("city").value,
                    country_code: document.getElementById("country").value,
                    postal_code: document.getElementById("zip").value
                    phone: document.getElementById("phone").value,
                    state: document.getElementById("state").value,
                    email: document.getElementById("email").value
                  }
                }
              }
            ],
            note_to_payer: "Contact us for any questions on your order."
          });
        },
        onAuthorize: (data, actions) => {
          return actions.payment.execute().then(payment => {
            // Do something when payment is successful.
            // window.alert("Thank you for your purchase! You order will be processed and shipped as soon as possible");
            document.getElementById("myModal").style.display = "block";
            document.getElementById("ModalBackdrop").style.display = "block";
            this.cartSrvc.postCart();
          });
        }
      };
    
      addPaypalScript() {
        this.addScript = true;
        return new Promise((resolve, reject) => {
          const scripttagElement = document.createElement("script");
          scripttagElement.src = "https://www.paypalobjects.com/api/checkout.js";
          scripttagElement.onload = resolve;
          document.body.appendChild(scripttagElement);
        });
      }
    
      public getSubTotal() {
        this.subTotal = (document.getElementById("total").value) -
        ((document.getElementById("total").value * 0.07) +
        (document.getElementById("total").value * 0.03) +
        (document.getElementById("total").value * 0.01) +
        1.00);
        return this.subTotal.toFixed(2);
      }
    
      public getToken(): string {
        return localStorage.getItem("jwt");
      }
    }
    

    Of course, now I have a different error of malformed request, which placed inside of jsonlint and, as you can see, it came back as valid:

    From jsonlint.com

    But the main thing is the parameters/properties are working:

    along with subtotal calculations working

    If anyone can tell me what the new error is all about, then I would be grateful.

    Error: Request to post https://www.sandbox.paypal.com/v1/payments/payment failed with 400 error. Correlation id: f31569c675597, f31569c675597
    
    {
        "name": "MALFORMED_REQUEST",
        "message": "Incoming JSON request does not map to API request",
        "information_link": "https://developer.paypal.com/webapps/developer/docs/api/#MALFORMED_REQUEST",
        "debug_id": "f31569c675597"
    }
    
    request/</<@https://www.paypalobjects.com/api/checkout.js:14216:39
    

    Thanks once more to @paulsm4 for suggesting to look under the Network tab to see if the value was actually being posted.