DoorDash fees buster

Display DoorDash fees in a clearer way.

目前为 2025-01-12 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name DoorDash fees buster
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-01-11.2
  5. // @description Display DoorDash fees in a clearer way.
  6. // @author Somebody
  7. // @match https://www.doordash.com/consumer/checkout/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=doordash.com
  9. // @grant none
  10. // @run-at document-end
  11. // @license GNU GPLv3
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const D = window.document;
  18. const GLOBAL_TIMEOUT = 10 * 1000;
  19.  
  20. const KNOWN_MARKUP_BUSINESS_NAMES = [
  21. 'Popeyes Louisiana Kitchen',
  22. 'Jollibee',
  23. 'KFC',
  24. ];
  25.  
  26. const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  27.  
  28. const createElementWithInnerTextAndStyle = (ElementName, innerText, style = {}) => {
  29. const element = D.createElement(ElementName);
  30. if (typeof innerText === 'string') {
  31. element.innerText = innerText;
  32. }
  33. Object.assign(
  34. element.style,
  35. style
  36. );
  37. return element;
  38. };
  39.  
  40. const createToastWithText = (() => {
  41. const timeoutIdToElementMap = new Map();
  42. const showToast = (text, disappearDelayInMs = 1000) => {
  43. const defaultStyle = {
  44. position: 'fixed',
  45. top: '10vh',
  46. zIndex: 9999,
  47. transform: 'translateX(-50%)',
  48. left: '50%',
  49. transition: 'all 0.25s ease',
  50. borderRadius: '2em',
  51. backgroundColor: 'black',
  52. padding: '0.5em 2em',
  53. color: 'white',
  54. textAlign: 'center',
  55. opacity: '0',
  56. };
  57. const toast = createElementWithInnerTextAndStyle('div', text, defaultStyle);
  58. D.body.append(toast);
  59. const timeoutId = setTimeout(
  60. () => {
  61. timeoutIdToElementMap.delete(timeoutId);
  62. D.body.removeChild(toast);
  63. },
  64. disappearDelayInMs
  65. );
  66. timeoutIdToElementMap.set(timeoutId, toast);
  67. setTimeout(
  68. () => {
  69. Object.assign(
  70. toast.style, {
  71. opacity: '100%'
  72. }
  73. );
  74. },
  75. 0
  76. );
  77. setTimeout(
  78. () => {
  79. Object.assign(
  80. toast.style, {
  81. opacity: '0'
  82. }
  83. );
  84. },
  85. disappearDelayInMs - 250
  86. );
  87. };
  88. return showToast;
  89. })();
  90.  
  91. const getInitialServiceFee = () => {
  92. // This is no longer true if items are modified after page load
  93. const keyword = 'Service Fee: ';
  94. let line = Array.from(D.querySelectorAll('script')).find(e => e.innerText.includes(keyword))?.innerText;
  95. line = line?.substring(line?.indexOf(keyword));
  96. line = line?.substring(0, line?.indexOf('\"'));
  97. return line;
  98. };
  99.  
  100. const getInitialEstimatedTax = () => {
  101. // This is no longer true if items are modified after page load
  102. const keyword = 'Estimated Tax: ';
  103. let line = Array.from(D.querySelectorAll('script')).find(e => e.innerText.includes(keyword))?.innerText;
  104. line = line?.substring(line?.indexOf(keyword));
  105. line = line?.substring(0, line?.indexOf('\"'));
  106. return line;
  107. };
  108.  
  109. const getInitialBusinessName = () => {
  110. const businessNameElement = Array.from(document.querySelectorAll('span')).find(spenElement => ['Your cart from', 'Order From', 'Checkout'].includes(spenElement.innerText))?.nextSibling;
  111. if (businessNameElement instanceof HTMLElement) {
  112. return Array.from(businessNameElement.querySelectorAll('span')).find(spenElement => typeof spenElement.innerText === 'string' && spenElement.innerText.length > 0)?.innerText;
  113. }
  114. };
  115.  
  116. const triggerHover = (element) => {
  117. if (element instanceof HTMLElement) {
  118. // Create the mouseenter event
  119. const mouseEnterEvent = new MouseEvent('mouseenter', {
  120. bubbles: true,
  121. cancelable: true,
  122. view: window,
  123. });
  124.  
  125. // Create the mouseover event
  126. const mouseOverEvent = new MouseEvent('mouseover', {
  127. bubbles: true,
  128. cancelable: true,
  129. view: window,
  130. });
  131.  
  132. // Create the mouseleave event
  133. const mouseLeaveEvent = new MouseEvent('mouseleave', {
  134. bubbles: true,
  135. cancelable: true,
  136. view: window,
  137. });
  138.  
  139. // Dispatch mouseenter and mouseover events
  140. element.dispatchEvent(mouseEnterEvent);
  141. element.dispatchEvent(mouseOverEvent);
  142.  
  143. const triggerHoverEnd = () => {
  144. element.dispatchEvent(mouseLeaveEvent);
  145. };
  146. return triggerHoverEnd;
  147. }
  148. };
  149.  
  150. const findLineItemsElement = () => D.querySelector('[data-testid="LineItems"]');
  151.  
  152. // const findStackChildrenElement = () => D.querySelector('[class^="StackChildren"]');
  153.  
  154. // const findTooltipElements = () => D.querySelectorAll('[role="tooltip"]');
  155.  
  156. const findFeesAndEstimatedTaxElement = () => D.querySelector('[data-testid="Fees & Estimated Tax"]');
  157.  
  158. const findFeesAndEstimatedTaxTooltipTriggerElement = () => findFeesAndEstimatedTaxElement()?.querySelector('button');
  159.  
  160. const findSubtotalElement = () => D.querySelector('[data-testid="Subtotal"]');
  161.  
  162. const findDeliveryFeeElement = () => D.querySelector('[data-testid="Delivery Fee"]');
  163.  
  164. const findLongDistanceFeeElement = () => D.querySelector('[data-testid="Long Distance Fee"]');
  165.  
  166. const findDasherTipElement = () => D.querySelector('[data-testid="Dasher Tip"]');
  167.  
  168. const findTotalElement = () => D.querySelector('[data-testid="Total"]');
  169.  
  170. const findCartTotalElement = () => D.querySelector('[data-anchor-id="OrderCartTotal"]');
  171.  
  172. const findPopupLayerElement = () => D.querySelector('[data-testid="LAYER-MANAGER-POPOVER_CONTENT"]');
  173.  
  174. const findFeesAndEstimatedTaxTotalElement = () => {
  175. const feesAndEstimatedTaxElement = findFeesAndEstimatedTaxElement();
  176. if (feesAndEstimatedTaxElement instanceof HTMLElement) {
  177. const totalElement = Array.from(feesAndEstimatedTaxElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
  178. return totalElement;
  179. }
  180. return undefined;
  181. };
  182.  
  183. const findSubtotalTotalElement = () => {
  184. const subtotalElement = findSubtotalElement();
  185. if (subtotalElement instanceof HTMLElement) {
  186. const totalElement = Array.from(subtotalElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
  187. return totalElement;
  188. }
  189. return undefined;
  190. };
  191.  
  192. const findDoorDashCreditsTotalElement = () => {
  193. const lineItemsElement = D.querySelector('[data-testid="LineItems"]');
  194. if (lineItemsElement instanceof HTMLElement) {
  195. const doorDashCreditsDescriptionElement = Array.from(lineItemsElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('DoorDash Credits'));
  196. const doorDashCreditsTotalElement = doorDashCreditsDescriptionElement?.parentNode?.lastChild;
  197. return doorDashCreditsTotalElement;
  198. }
  199. return undefined;
  200. };
  201.  
  202. const findDasherTipTotalElement = () => {
  203. const dasherTipElement = findDasherTipElement();
  204. if (dasherTipElement instanceof HTMLElement) {
  205. const doorDashTipTotalElement = Array.from(dasherTipElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$'));
  206. return doorDashTipTotalElement;
  207. }
  208. return undefined;
  209. }
  210.  
  211. const findDeliveryFeeElementTotalElement = () => {
  212. const deliveryFeeElement = findDeliveryFeeElement();
  213. if (deliveryFeeElement instanceof HTMLElement) {
  214. const deliveryFeeTotalElement = Array.from(deliveryFeeElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
  215. return deliveryFeeTotalElement;
  216. }
  217. return undefined;
  218. };
  219.  
  220. const findLongDistanceFeeTotalElement = () => {
  221. const longDistanceFeeElement = findLongDistanceFeeElement();
  222. if (longDistanceFeeElement instanceof HTMLElement) {
  223. const longDistanceFeeTotalElement = Array.from(longDistanceFeeElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
  224. return longDistanceFeeTotalElement;
  225. }
  226. return undefined;
  227. };
  228.  
  229. const findTooltipByKeywordsWithObserver = (keywords) => new Promise((resolve, reject) => {
  230. const popupLayerElement = findPopupLayerElement();
  231. if (popupLayerElement instanceof HTMLElement) {
  232. const timeoutId = setTimeout(() => {
  233. createToastWithText('Unable to find target popup element before timeout');
  234. reject(new Error('unable to find target popup element before timeout'));
  235. observer.disconnect();
  236. }, GLOBAL_TIMEOUT);
  237. const observer = new MutationObserver(async (mutationsList, observer) => {
  238. try {
  239. // Iterate over all mutations
  240. loop:
  241. for (let mutation of mutationsList) {
  242. // Check if nodes were added to the DOM
  243. if (mutation.type === 'childList') {
  244. // Look for the element with the specific keyword
  245. for (const addedNode of mutation.addedNodes) {
  246. if (keywords.every(keyword => addedNode.innerText?.includes(keyword))) {
  247. // disconnect the observer once the element is found
  248. observer.disconnect();
  249. clearTimeout(timeoutId);
  250. resolve(addedNode);
  251. break loop;
  252. }
  253. }
  254. }
  255. }
  256. } catch (err) {
  257. clearTimeout(timeoutId);
  258. createToastWithText('Unable to find tooltip due to an error');
  259. console.error(err);
  260. reject(err);
  261. }
  262. });
  263.  
  264. // Configure the observer to look for added nodes in the popup layer
  265. observer.observe(popupLayerElement, {
  266. childList: true, // Look for added/removed child nodes
  267. subtree: true, // Include all descendants in the search
  268. });
  269. } else {
  270. reject(new Error('unable to find popup layer element'));
  271. }
  272. });
  273.  
  274. const getServiceFeeAndEstimatedTaxFromTooltip = async () => {
  275. const targetTooltipElementPromise = findTooltipByKeywordsWithObserver(['Service Fee', 'Estimated Tax']);
  276. const triggerHoverEnd = triggerHover(findFeesAndEstimatedTaxTooltipTriggerElement());
  277. const targetTooltipElement = await targetTooltipElementPromise;
  278. const targetTooltipElementInnerText = targetTooltipElement?.innerText;
  279. triggerHoverEnd?.();
  280. const targetTooltipElementInnerTextLines = targetTooltipElementInnerText?.split('\n')?.map(line => line.trim());
  281. const serviceFee = targetTooltipElementInnerTextLines?.find(line => line.startsWith('Service Fee'));
  282. const estimatedTax = targetTooltipElementInnerTextLines?.find(line => line.startsWith('Estimated Tax'));
  283. return [serviceFee, estimatedTax];
  284. };
  285.  
  286. const getNumberOnly = (text) => {
  287. const decimalNumberRegExp = new RegExp(/(\d*\.)?\d+/);
  288. if (typeof text !== 'string') {
  289. throw new TypeError('text must be string');
  290. }
  291. // Find the last index of $, to get only the original price
  292. const originalNumberValue = text.substring(text.lastIndexOf('$'));
  293. const numberPart = originalNumberValue.match(decimalNumberRegExp)?.[0];
  294. return numberPart;
  295. };
  296.  
  297. const getServiceFeeAndEstimatedTaxFromTooltipMemorized = (() => {
  298. let serviceFee = 'Fee: $0.0';
  299. let estimatedTax = 'Tax: $0.0';
  300. let isFirstRun = true;
  301. return async () => {
  302. const serviceFeeAndEstimatedTax = findFeesAndEstimatedTaxTotalElement()?.innerText;
  303. // Be careful about float point number precision error
  304. if (Math.round(parseFloat(getNumberOnly(serviceFeeAndEstimatedTax)) * 100) !== Math.round(parseFloat(getNumberOnly(serviceFee)) * 100) + Math.round(parseFloat(getNumberOnly(estimatedTax)) * 100)) {
  305. if (isFirstRun) {
  306. [serviceFee, estimatedTax] = [getInitialServiceFee(), getInitialEstimatedTax()];
  307. isFirstRun = false;
  308. if (typeof serviceFee === 'undefined' || typeof estimatedTax === 'undefined') {
  309. [serviceFee, estimatedTax] = await getServiceFeeAndEstimatedTaxFromTooltip();
  310. }
  311. } else {
  312. [serviceFee, estimatedTax] = await getServiceFeeAndEstimatedTaxFromTooltip();
  313. }
  314. }
  315. return [serviceFee, estimatedTax];
  316. };
  317. })();
  318. const getTotalCost = () => {
  319. const cartTotalElement = findCartTotalElement();
  320. const cartTotal = parseFloat(getNumberOnly(cartTotalElement.innerText));
  321. let totalCost = cartTotal;
  322. const doorDashCreditsTotalElement = findDoorDashCreditsTotalElement();
  323. if (doorDashCreditsTotalElement instanceof HTMLElement) {
  324. const doorDashCreditsTotal = Math.abs(parseFloat(getNumberOnly(doorDashCreditsTotalElement.innerText)));
  325. totalCost += doorDashCreditsTotal;
  326. }
  327. return totalCost;
  328. };
  329.  
  330. const insertNewRow = (description, total, insertAfterElement, testid = '') => {
  331. const newRowElement = findSubtotalElement().cloneNode(true);
  332. newRowElement.dataset.testid = testid;
  333. const descriptionElement = Array.from(newRowElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('Subtotal'));
  334. descriptionElement.innerText = description;
  335. const totalElement = Array.from(newRowElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$'));
  336. totalElement.innerText = total;
  337. if (insertAfterElement.nextSibling) {
  338. insertAfterElement.parentNode.insertBefore(newRowElement, insertAfterElement.nextSibling);
  339. } else {
  340. insertAfterElement.parentNode.appendChild(newRowElement);
  341. }
  342. return newRowElement;
  343. };
  344.  
  345. const calcCostPercentage = (cost) => {
  346. const subtotalTotalElement = findSubtotalTotalElement();
  347. const subtotal = parseFloat(getNumberOnly(subtotalTotalElement.innerText));
  348. const percentage = Math.ceil(cost * 10000 / subtotal) / 100;
  349. return percentage;
  350. };
  351.  
  352. const setColorRecursively = (parentElement, color = 'red') => {
  353. Array.from(parentElement.querySelectorAll('span, div')).forEach(spanOrDivElement => {
  354. Object.assign(
  355. spanOrDivElement.style,
  356. {
  357. color: color,
  358. },
  359. );
  360. });
  361. };
  362.  
  363. const updateLineItems = async () => {
  364. const feesAndEstimatedTaxElement = findFeesAndEstimatedTaxElement();
  365. const [serviceFee, estimatedTax] = await getServiceFeeAndEstimatedTaxFromTooltipMemorized();
  366. if (feesAndEstimatedTaxElement instanceof HTMLElement) {
  367. const serviceFeePercentage = calcCostPercentage(parseFloat(getNumberOnly(serviceFee.split(': ')[1])));
  368. const feesElementTestid = 'Fees';
  369. D.querySelector(`[data-testid="${feesElementTestid}"]`)?.remove();
  370. const feesElement = insertNewRow(serviceFee.split(': ')[0], `${serviceFeePercentage}% = $${getNumberOnly(serviceFee)}`, feesAndEstimatedTaxElement, feesElementTestid);
  371. setColorRecursively(feesElement, 'red');
  372. const estimatedTaxPercentage = calcCostPercentage(parseFloat(getNumberOnly(estimatedTax.split(': ')[1])));
  373. const estimatedTaxElementTestid = 'Estimated Tax';
  374. D.querySelector(`[data-testid="${estimatedTaxElementTestid}"]`)?.remove();
  375. const estimatedTaxElement = insertNewRow(estimatedTax.split(': ')[0], `${estimatedTaxPercentage}% = $${getNumberOnly(estimatedTax)}`, feesElement, estimatedTaxElementTestid);
  376.  
  377. setColorRecursively(feesAndEstimatedTaxElement, 'darkgray');
  378. }
  379.  
  380. const deliveryFeeElement = findDeliveryFeeElement();
  381. if (deliveryFeeElement instanceof HTMLElement) {
  382. const deliveryFeeTotalElement = findDeliveryFeeElementTotalElement();
  383. deliveryFeeTotalElement.innerText = `${calcCostPercentage(parseFloat(getNumberOnly(deliveryFeeTotalElement.innerText)))}% = $${getNumberOnly(deliveryFeeTotalElement.innerText)}`;
  384. setColorRecursively(deliveryFeeElement, 'red');
  385. }
  386.  
  387. const longDistanceFeeElement = findLongDistanceFeeElement();
  388. if (longDistanceFeeElement instanceof HTMLElement) {
  389. const longDistanceFeeElement = findLongDistanceFeeTotalElement();
  390. longDistanceFeeElement.innerText = `${calcCostPercentage(parseFloat(getNumberOnly(longDistanceFeeElement.innerText)))}% = $${getNumberOnly(longDistanceFeeElement.innerText)}`;
  391. setColorRecursively(longDistanceFeeElement, 'red');
  392. }
  393.  
  394. const dasherTipElement = findDasherTipElement();
  395. if (dasherTipElement instanceof HTMLElement) {
  396. const dasherTipTotalElement = findDasherTipTotalElement();
  397. dasherTipTotalElement.innerText = `${calcCostPercentage(parseFloat(getNumberOnly(dasherTipTotalElement.innerText)))}% = $${getNumberOnly(dasherTipTotalElement.innerText)}`;
  398. setColorRecursively(dasherTipElement, 'red');
  399. }
  400.  
  401. const totalElement = findTotalElement();
  402. if (totalElement instanceof HTMLElement) {
  403. const totalCost = getTotalCost();
  404. const cartTotalElement = findCartTotalElement();
  405. cartTotalElement.innerText = `${calcCostPercentage(totalCost - getNumberOnly(estimatedTax.split(': ')[1]))}% (Pretax) / ${calcCostPercentage(totalCost)}% = $${getNumberOnly(cartTotalElement.innerText)}`;
  406. const businessName = getInitialBusinessName();
  407. if (KNOWN_MARKUP_BUSINESS_NAMES.includes(businessName)) {
  408. const markUpWarningTestid = 'Markup Warning';
  409. D.querySelector(`[data-testid="${markUpWarningTestid}"]`)?.remove();
  410. const markUpWarningElement = insertNewRow('This business is known to charge more for DoorDash delivery, the actual pretax subtotal would be even lower in store.\n(Hint: Switch to pickup and take a look?)', ' ', totalElement, markUpWarningTestid);
  411. setColorRecursively(markUpWarningElement, 'red');
  412. }
  413. }
  414. };
  415.  
  416. const restoreLineItems = () => {
  417. // const feesElementTestid = 'Fees';
  418. // D.querySelector(`[data-testid="${feesElementTestid}"]`)?.remove();
  419. // const estimatedTaxElementTestid = 'Estimated Tax';
  420. // D.querySelector(`[data-testid="${estimatedTaxElementTestid}"]`)?.remove();
  421. const feesAndEstimatedTaxElement = findFeesAndEstimatedTaxElement();
  422. if (feesAndEstimatedTaxElement instanceof HTMLElement) {
  423. setColorRecursively(feesAndEstimatedTaxElement, 'initial');
  424. }
  425.  
  426. const deliveryFeeElement = findDeliveryFeeElement();
  427. if (deliveryFeeElement instanceof HTMLElement) {
  428. const deliveryFeeTotalElement = findDeliveryFeeElementTotalElement();
  429. deliveryFeeTotalElement.innerText = `$${getNumberOnly(deliveryFeeTotalElement.innerText)}`;
  430. setColorRecursively(deliveryFeeElement, 'initial');
  431. }
  432.  
  433. const longDistanceFeeElement = findLongDistanceFeeElement();
  434. if (longDistanceFeeElement instanceof HTMLElement) {
  435. const longDistanceFeeElement = findLongDistanceFeeTotalElement();
  436. longDistanceFeeElement.innerText = `$${getNumberOnly(longDistanceFeeElement.innerText)}`;
  437. setColorRecursively(longDistanceFeeElement, 'initial');
  438. }
  439.  
  440. const dasherTipElement = findDasherTipElement();
  441. if (dasherTipElement instanceof HTMLElement) {
  442. const dasherTipTotalElement = findDasherTipTotalElement();
  443. dasherTipTotalElement.innerText = `$${getNumberOnly(dasherTipTotalElement.innerText)}`;
  444. setColorRecursively(dasherTipElement, 'initial');
  445. }
  446.  
  447. const totalElement = findTotalElement();
  448. if (totalElement instanceof HTMLElement) {
  449. const totalCost = getTotalCost();
  450. const cartTotalElement = findCartTotalElement();
  451. cartTotalElement.innerText = `$${getNumberOnly(cartTotalElement.innerText)}`;
  452. const markUpWarningTestid = 'Markup Warning';
  453. D.querySelector(`[data-testid="${markUpWarningTestid}"]`)?.remove();
  454. }
  455. };
  456.  
  457. const main = () => {
  458. const timeoutId = setTimeout(() => {
  459. createToastWithText('Failed to display Fees & Estimated Tax before timeout');
  460. }, GLOBAL_TIMEOUT);
  461. const observer = new MutationObserver(async (mutationsList, observer) => {
  462. try {
  463. // Iterate over all mutations
  464. for (let mutation of mutationsList) {
  465. // Check if nodes were added to the DOM
  466. if (mutation.type === 'childList') {
  467. // Look for the element with the specific data-testid
  468. const lineItemsElement = findLineItemsElement();
  469. if (lineItemsElement instanceof HTMLElement) {
  470. // disconnect the observer once the element is found
  471. observer.disconnect();
  472. try {
  473. await updateLineItems();
  474. } catch (err) {
  475. console.error(err);
  476. }
  477.  
  478. const lineItemsElementObserverOptions = {
  479. childList: true, // Look for added/removed child nodes
  480. subtree: true, // Include all descendants in the search
  481. characterData: true,
  482. };
  483.  
  484. const lineItemsObserver = new MutationObserver(async (lineItemsMutationsList, lineItemsObserver) => {
  485. // Avoid recursive calls
  486. lineItemsObserver.disconnect();
  487. try {
  488. await updateLineItems();
  489. } catch (err) {
  490. console.error(err);
  491. restoreLineItems();
  492. }
  493. lineItemsObserver.observe(lineItemsElement, lineItemsElementObserverOptions);
  494. });
  495.  
  496. lineItemsObserver.observe(lineItemsElement, lineItemsElementObserverOptions);
  497.  
  498. clearTimeout(timeoutId);
  499. createToastWithText('Fees & Estimated Tax rate calculated');
  500.  
  501. break;
  502. }
  503. }
  504. }
  505. } catch (err) {
  506. clearTimeout(timeoutId);
  507. createToastWithText('Failed to display Fees & Estimated Tax due to an error');
  508. console.error(err);
  509. }
  510. });
  511.  
  512. // Configure the observer to look for added nodes in the entire document
  513. observer.observe(D.body, {
  514. childList: true, // Look for added/removed child nodes
  515. subtree: true, // Include all descendants in the search
  516. });
  517. };
  518.  
  519. main();
  520. })();